为什么 nil/NULL 块在运行时会导致总线错误?

我开始大量使用块,很快就注意到空块会导致总线错误:

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

这似乎违背了 Objective-C 通常忽略消息到空对象的行为:

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

因此,在使用块之前,我必须使用通常的 nil 检查:

if (aBlock != nil)
aBlock();

或者使用假的积木:

aBlock = ^{};
aBlock(); // runs fine

还有其他选择吗? 为什么 nil 块不能简单地表示为 nop?

15636 次浏览

注意: 我不是积木方面的专家。

目标 c 目标但调用 块是 < em > not 消息,尽管您仍然可以尝试 [block retain]ing 一个 nil块或其他消息。

希望这(和链接)有所帮助。

这是我最好的简单解决方案... ... 也许可以用这些 c var-args 编写一个通用 run 函数,但我不知道如何编写。

void run(void (^block)()) {
if (block)block();
}


void runWith(void (^block)(id), id value) {
if (block)block(value);
}

Matt Galloway 的答案太完美了! 好书!

我只想补充一点,有一些方法可以让生活变得更简单。你可以像这样定义一个宏:

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil

它可以接受0-n 个参数

typedef void (^SimpleBlock)(void);
SimpleBlock simpleNilBlock = nil;
SimpleBlock simpleLogBlock = ^{ NSLog(@"working"); };
BLOCK_SAFE_RUN(simpleNilBlock);
BLOCK_SAFE_RUN(simpleLogBlock);


typedef void (^BlockWithArguments)(BOOL arg1, NSString *arg2);
BlockWithArguments argumentsNilBlock = nil;
BlockWithArguments argumentsLogBlock = ^(BOOL arg1, NSString *arg2) { NSLog(@"%@", arg2); };
BLOCK_SAFE_RUN(argumentsNilBlock, YES, @"ok");
BLOCK_SAFE_RUN(argumentsLogBlock, YES, @"ok");

如果 你想得到 块的返回值,但是你不确定这个块是否存在,那么你最好还是输入:

block ? block() : nil;

这样就可以很容易地定义回退值。

我想对这个问题进行更多的解释,并给出一个更完整的答案:

#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 消息已经经过了蹦床,因为它们需要查找实际将被调用的方法。运行时允许延迟注入方法和更改方法实现,因此它已经通过了蹦床。在这种情况下,执行空检查的额外代价并不显著。

我希望这能帮你解释一下原因。

有关详细信息,请参阅我的 博客 职位