我想用更完整的答案对此进行更多解释。首先让我们考虑一下这段代码:
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
void (^block)() = nil;
block();
}
如果你运行这个,你会看到崩溃block()
看起来像这样的行(当在 32 位架构上运行时 - 这很重要):
EXC_BAD_ACCESS(代码=2,地址=0xc)
那么,这是为什么呢?嗯,0xc
是最重要的一点。崩溃意味着处理器已尝试读取内存地址处的信息0xc
。这几乎肯定是完全错误的做法。那里不太可能有什么东西。但为什么它要尝试读取这个内存位置呢?嗯,这是由于块在引擎盖下实际构建的方式造成的。
定义块时,编译器实际上在堆栈上创建一个结构,其形式如下:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
该块是指向该结构的指针。第四位成员,invoke
,这个结构是有趣的。它是一个函数指针,指向保存块实现的代码。因此,当调用块时,处理器会尝试跳转到该代码。请注意,如果您计算结构中之前的字节数invoke
成员,你会发现十进制有12,或者十六进制有C。
因此,当调用一个块时,处理器会获取该块的地址,加 12 并尝试加载该内存地址中保存的值。然后它尝试跳转到该地址。但如果该块为零,那么它将尝试读取地址0xc
。显然,这是一个错误的地址,因此我们得到了分段错误。
现在,它一定是这样的崩溃,而不是像 Objective-C 消息调用那样默默地失败,这实际上是一种设计选择。由于编译器正在决定如何调用该块,因此它必须在调用块的每个地方注入 nil 检查代码。这会增加代码大小并导致性能下降。另一种选择是使用蹦床来进行 nil 检查。然而,这也会导致性能损失。 Objective-C 消息已经通过了蹦床,因为它们需要查找实际将被调用的方法。运行时允许延迟注入方法和更改方法实现,因此无论如何它已经经历了蹦床。在这种情况下,进行 nil 检查的额外惩罚并不重要。
欲了解更多信息,请参阅我的blog http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/ posts http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-2/.