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

时间:2010-11-10 13:53:00

标签: objective-c objective-c-blocks

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

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

这似乎违反了Objective-C的通常行为,忽略了对nil对象的消息:

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

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

if (aBlock != nil)
    aBlock();

或使用虚拟块:

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

还有其他选择吗?有没有理由为什么nil block不能简单地成为nop?

4 个答案:

答案 0 :(得分:139)

我想用更完整的答案解释一下这个问题。首先让我们考虑一下这段代码:

#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。这显然是一个duff地址,因此我们得到了分段错误。

现在它必须像这样崩溃而不是像Objective-C消息调用一样无声地失败的原因确实是一个设计选择。由于编译器正在执行决定如何调用块的工作,因此必须在调用块的任何地方注入nil检查代码。这会增加代码大小并导致性能不佳。另一种选择是使用蹦床进行零检查。然而,这也会导致性能损失。 Objective-C消息已经通过蹦床,因为他们需要查找实际调用的方法。运行时允许延迟注入方法和更改方法实现,因此无论如何它已经通过蹦床了。在这种情况下,进行零检查的额外惩罚并不重要。

我希望这有助于解释一下理由。

有关详细信息,请参阅我的blog posts

答案 1 :(得分:39)

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;

这样您就可以轻松定义回退值。在我的例子中'nil'。

答案 2 :(得分:8)

警告:我不是Block的专家。

阻止 objective-c objects但是调用了block is not a message,尽管您仍然可以[block retain] nil阻止或其他消息。

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

答案 3 :(得分:2)

这是我最简单的最好的解决方案......也许有可能用那些c var-args编写一个通用运行函数,但我不知道怎么写。

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

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