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

zou*_*oul 72 objective-c objective-c-blocks

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

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error
Run Code Online (Sandbox Code Playgroud)

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

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

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

if (aBlock != nil)
    aBlock();
Run Code Online (Sandbox Code Playgroud)

或者使用虚拟块:

aBlock = ^{};
aBlock(); // runs fine
Run Code Online (Sandbox Code Playgroud)

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

mat*_*way 140

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

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {    
    void (^block)() = nil;
    block();
}
Run Code Online (Sandbox Code Playgroud)

如果你运行这个,那么你会看到一个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. */
};
Run Code Online (Sandbox Code Playgroud)

然后该块是指向该结构的指针.invoke这个结构的第四个成员是有趣的.它是一个函数指针,指向块的实现所在的代码.因此,处理器尝试在调用块时跳转到该代码.请注意,如果计算invoke成员之前结构中的字节数,您会发现十进制中有12个,或者十六进制中的C.

因此,当调用块时,处理器获取块的地址,添加12并尝试加载保存在该内存地址的值.然后它尝试跳转到该地址.但是如果块是零,那么它将尝试读取地址0xc.这显然是一个duff地址,因此我们得到了分段错误.

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

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

有关更多信息,请参阅我的博客 文章.


hfo*_*sli 39

Matt Galloway的回答非常完美!好读!

我只想补充说,有一些方法可以让生活更轻松.您可以像这样定义一个宏:

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil
Run Code Online (Sandbox Code Playgroud)

它可能需要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");
Run Code Online (Sandbox Code Playgroud)

如果你想获得返回值并且你不确定块是否存在那么你可能最好只输入:

block ? block() : nil;
Run Code Online (Sandbox Code Playgroud)

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


Ste*_*ani 9

警告:我不是Block的专家.

Objective-C的对象,但调用块是没有消息,但你仍然可以尝试[block retain]荷兰国际集团一个nil块或其他消息.

希望,(和链接)有所帮助.

  • 我一般只是阻止?block():nil; 这对我来说足够简洁,并且在你做的事情上是透明的...... (3认同)