我想用一个更完整的答案来进一步解释这一点。首先让我们考虑以下代码:
#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;
};
然后,该块是指向该结构的指针。invoke
这个结构的第四个成员有趣。它是一个函数指针,指向保存该块实现的代码。因此,在调用一个块时,处理器会尝试跳转到该代码。请注意,如果您在invoke
成员之前计算结构中的字节数,您会发现十进制为12,十六进制为C。
因此,当调用一个块时,处理器将获取该块的地址,将其加12,然后尝试加载该内存地址中保存的值。然后,它尝试跳转到该地址。但是,如果该块为nil,它将尝试读取该地址0xc
。显然,这是达芙地址,因此我们得到了分段错误。
现在,它一定是像这样崩溃的原因,而不是像Objective-C消息调用那样静默失败的原因,这实际上是一种设计选择。由于编译器正在完成决定如何调用该块的工作,因此必须在调用块的任何地方注入nil检查代码。这会增加代码大小并导致性能下降。另一种选择是使用蹦床进行零位检查。但是,这也会导致性能下降。Objective-C消息已经通过了蹦床,因为它们需要查找将实际调用的方法。运行时允许方法的惰性注入和方法实现的更改,因此无论如何它已经通过了蹦床。在这种情况下,执行nil检查的额外代价并不重要。
我希望这可以帮助您解释其基本原理。
有关更多信息,请参阅我的博客 文章。