为何调用block应先判空

得益于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也是一个对象。更进一步地说,它是一个结构体。基本结构如下:

struct Block_literal_1 {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 {
        unsigned long int reserved;     // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        // optional helper functions
        void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        void (*dispose_helper)(void *src);             // IFF (1<<25)
        // required ABI.2010.3.16
        const char *signature;                         // IFF (1<<30)
    } *descriptor;
    // imported variables
};

从名字可以看到invoke就是要执行的函数地址。为了紧紧围绕主题(实则偷懒),就不涉及其余的变量和类似变量捕获等的细节。

对于下面这么简单的一句hello world

^ { printf("hello world\n"); }

其实最终会被编译器处理成如下形式:

struct __block_literal_1 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(struct __block_literal_1 *);
    struct __block_descriptor_1 *descriptor;
};

void __block_invoke_1(struct __block_literal_1 *_block) {
    printf("hello world\n");
}

static struct __block_descriptor_1 {
    unsigned long int reserved;
    unsigned long int Block_size;
} __block_descriptor_1 = { 0, sizeof(struct __block_literal_1), __block_invoke_1 };

事实上,上面这个block的调用就是调用了函数__block_invoke_1并且将__block_literal_1作为参数传进去(当然更复杂的block会有更多的参数)。那么最终调用到底是怎样的呢?

调用细节

考虑下面这样一个简单的block调用:

- (void)blockInvoke:(void (^)(void))block
{
    block();
}

其在模拟器上运行时的汇编代码为如下形式:

    0x101daf510 <+0>:  pushq  %rbp
    0x101daf511 <+1>:  movq   %rsp, %rbp
    0x101daf514 <+4>:  subq   $0x20, %rsp
    0x101daf518 <+8>:  leaq   -0x18(%rbp), %rax
    0x101daf51c <+12>: movq   %rdi, -0x8(%rbp)
    0x101daf520 <+16>: movq   %rsi, -0x10(%rbp)
    0x101daf524 <+20>: movq   $0x0, -0x18(%rbp)
    0x101daf52c <+28>: movq   %rax, %rdi
    0x101daf52f <+31>: movq   %rdx, %rsi
->  0x101daf532 <+34>: callq  0x101db01e4               ; symbol stub for: objc_storeStrong
    0x101daf537 <+39>: movq   -0x18(%rbp), %rax
    0x101daf53b <+43>: movq   %rax, %rdx
    0x101daf53e <+46>: movq   %rdx, %rdi
->  0x101daf541 <+49>: callq  *0x10(%rax)
    0x101daf544 <+52>: xorl   %ecx, %ecx
    0x101daf546 <+54>: movl   %ecx, %esi
    0x101daf548 <+56>: leaq   -0x18(%rbp), %rax
    0x101daf54c <+60>: movq   %rax, %rdi
    0x101daf54f <+63>: callq  0x101db01e4               ; symbol stub for: objc_storeStrong
    0x101daf554 <+68>: addq   $0x20, %rsp
    0x101daf558 <+72>: popq   %rbp
    0x101daf559 <+73>: retq   

可以知道在0x101daf541处是调用block的指令。那么0x10(%rax)即为所要调用的函数地址。往前还有一个objc_storeStrong调用(0x101daf532处),它做了什么呢?我们来看一下:

libobjc.A.dylib`objc_storeStrong:
    0x10e4f5cda <+0>:  pushq  %rbp
    0x10e4f5cdb <+1>:  movq   %rsp, %rbp
    0x10e4f5cde <+4>:  pushq  %r15
    0x10e4f5ce0 <+6>:  pushq  %r14
    0x10e4f5ce2 <+8>:  pushq  %rbx
    0x10e4f5ce3 <+9>:  pushq  %rax
    0x10e4f5ce4 <+10>: movq   %rsi, %rbx
    0x10e4f5ce7 <+13>: movq   %rdi, %r15
    0x10e4f5cea <+16>: movq   (%r15), %r14
    0x10e4f5ced <+19>: cmpq   %rbx, %r14
    0x10e4f5cf0 <+22>: je     0x10e4f5d0f               ; <+53>
    0x10e4f5cf2 <+24>: movq   %rbx, %rdi
    0x10e4f5cf5 <+27>: callq  0x10e4f5cb0               ; objc_retain
    0x10e4f5cfa <+32>: movq   %rbx, (%r15)
    0x10e4f5cfd <+35>: movq   %r14, %rdi
    0x10e4f5d00 <+38>: addq   $0x8, %rsp
    0x10e4f5d04 <+42>: popq   %rbx
    0x10e4f5d05 <+43>: popq   %r14
    0x10e4f5d07 <+45>: popq   %r15
    0x10e4f5d09 <+47>: popq   %rbp
    0x10e4f5d0a <+48>: jmp    0x10e4f5d20               ; objc_release
    0x10e4f5d0f <+53>: addq   $0x8, %rsp
    0x10e4f5d13 <+57>: popq   %rbx
    0x10e4f5d14 <+58>: popq   %r14
    0x10e4f5d16 <+60>: popq   %r15
    0x10e4f5d18 <+62>: popq   %rbp
    0x10e4f5d19 <+63>: retq   
    0x10e4f5d1a <+64>: nopw   (%rax,%rax)

虽然也可以分析下汇编,但还是尽量避免这种分析吧,搜一搜objc_storeStrong能够找到clang文档关于objc_storeStrong的实现如下:

id objc_storeStrong(id *object, id value) {
  value = [value retain];
  id oldValue = *object;
  *object = value;
  [oldValue release];
  return value;
}

从上面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:本以为没啥东西的最后扯了这么一堆,我也是够了……>_<

参考资料