得益于Objective-C的Runtime系统,对象的方法调用通过消息传递的机制进行,从而避免了很多情况下的判空操作。block也是Objective-C世界的对象,然而如果当它为空时,调用它却能导致崩溃。归根结底,其实是因为block的调用并不使用消息传递那套机制,而是通过跳转到特定的函数地址进行函数调用。我们可以先这么粗略的解释一下,然后再深入一点进行探讨。
函数调用和消息传递
Objective-C中的消息传递是类似[[obj method1:parameter1] method]
这样的形式,而普通的函数调用形如func2(func1(parameter1))
。而对于block,其自身虽然是作为对象存在,但其调用形式却与函数调用相同:block2(block1(parameter1))
。
对于消息传递,其使用的机制为查表,在Method Swizzling一文中简单提到过。其优点在于不用在编译时确定,而是在运行时决定所调用的方法。而且对nil发消息,返回的永远是nil。这样一来,灵活性极高,并且对于nil的处理方便许多。当然其缺点显而易见,虽然有缓存机制,还是会比直接调用效率低那么一些。
而函数调用,其调用入口地址在编译期就决定了,这种形式缺少了些灵活性,但是却是效率较高的一种形式。鉴于现在的硬件条件已经足够高,低频率的调用所带来的性能提升其实无关紧要(然而对于高频调用情况,还是需要更进一步的优化)。
作为block,虽然它作为堂堂的Objective-C对象,却并不使用消息传递机制。原因之一可能是它同时作为C扩展的一部分,用了函数调用的形式更通用;另外其效率可能也在考虑范围,毕竟block用处较多,广泛用于各种回调、遍历之中。当然最重要的是,其实现的结构就决定了其调用形式。
block结构
其实block也是一个对象。更进一步地说,它是一个结构体。基本结构如下:
从名字可以看到invoke就是要执行的函数地址。为了紧紧围绕主题(实则偷懒),就不涉及其余的变量和类似变量捕获等的细节。
对于下面这么简单的一句hello world
:
其实最终会被编译器处理成如下形式:
事实上,上面这个block的调用就是调用了函数__block_invoke_1
并且将__block_literal_1
作为参数传进去(当然更复杂的block会有更多的参数)。那么最终调用到底是怎样的呢?
调用细节
考虑下面这样一个简单的block调用:
其在模拟器上运行时的汇编代码为如下形式:
可以知道在0x101daf541
处是调用block的指令。那么0x10(%rax)
即为所要调用的函数地址。往前还有一个objc_storeStrong
调用(0x101daf532处),它做了什么呢?我们来看一下:
虽然也可以分析下汇编,但还是尽量避免这种分析吧,搜一搜objc_storeStrong
能够找到clang文档关于objc_storeStrong
的实现如下:
从上面blockInvoke的汇编代码可以知道传入objc_storeStrong
函数的参数object为新分配的指针地址,value为传入的block地址。可知当block为nil时,object依旧为nil,但如果block不为nil的话,那么object则指向value retain操作后的地址。
回到blockInvoke汇编代码。可以知道-0x18(%rbp)
为block经过objc_storeStrong
后的内容。在block为nil时,其内容显然为0也就是nil。此时$rax = 0。这样对于0x10(%rax)
来说,其实访问了地址为0x10
的内存,自然会造成EXC_BAD_ACCESS(code=1, address=0x10)
的错误。
如果传入一个非空的block,则0x10的偏移正好对应十进制下16偏移,在64位系统下,对应的是sizeof(void *) + sizeof(int) * 2, 也就是8 + 4 * 2 = 16。此偏移刚好指向invoke变量。此时便正确调用了block。
所以最后要说的其实是:调用block的时候一定要记得判断是不是为空,避免不必要的崩溃。
PS:本以为没啥东西的最后扯了这么一堆,我也是够了……>_<
参考资料