我希望能够更详细地解释一下,给出一个更完整的答案。首先,让我们考虑这段代码:
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
void (^block)() = nil;
block();
}
如果你运行这个程序,你会在block()
这一行看到一个崩溃,类似于以下内容(在32位架构上运行-这很重要):
EXC_BAD_ACCESS(code=2,address=0xc)
那么,为什么会这样呢?好吧,0xc
是最重要的位。崩溃意味着处理器尝试读取内存地址0xc
处的信息。这几乎肯定是完全不正确的事情。那里可能没有任何东西。但是它为什么要尝试读取这个内存位置呢?嗯,这归功于块实际上在底层是如何构建的。
当定义一个块时,编译器实际上会在栈上创建一个结构,形式如下:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
};
然后,该块是指向此结构的指针。这个结构的第四个成员invoke
是有趣的部分。它是一个函数指针,指向块实现所在的代码。因此,处理器在调用块时尝试跳转到那段代码。请注意,如果您数一下invoke
成员之前结构中的字节数,您会发现在十进制中有12个字节,在十六进制中为C。
因此,当调用块时,处理器取块的地址,加上12,并尝试加载该内存地址中保存的值。然后它尝试跳转到该地址。但是,如果块为nil,则尝试读取地址0xc
。显然这是一个错误的地址,所以我们得到了段错误。
现在,它必须像这样崩溃,而不是像Objective-C消息调用那样默默地失败,实际上是一种设计选择。由于编译器正在决定如何调用块,因此它必须在每次调用块时注入nil检查代码。这将增加代码大小并导致性能下降。另一种选择是使用一个跳板来进行nil检查。但是,这也会造成性能损失。Objective-C消息已经通过跳板进行了处理,因为它们需要查找将实际被调用的方法。运行时允许懒惰注入方法和改变方法实现,因此已经通过跳板进行处理了。在这种情况下执行nil检查的额外开销并不重要。
有关更多信息,请参见我的博客文章。