2010-11-10 92 views
67

我开始使用块了很多,很快就注意到了零块导致总线错误:为什么nil/NULL块在运行时会导致总线错误?

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

这似乎违背的Objective-C的日常行为,忽略消息无对象:

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

因此,我不得不求助于通常为零检查之前,我使用块:

if (aBlock != nil) 
    aBlock(); 

或者使用虚拟块:

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

还有其他的选择吗?为什么零块不能简单地是一个nop?

回答

134

我想解释一下这个,有一个更完整的答案。首先,让我们考虑下面的代码:

#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消息调用那样静静地失败确实是一个设计选择。由于编译器正在完成确定如何调用块的工作,因此必须在调用块的任何位置注入零检查代码。这会增加代码的大小并导致性能不佳。另一种选择是使用蹦床进行零检查。但是这也会导致性能损失。 Objective-C消息已经通过蹦床,因为它们需要查找实际将被调用的方法。运行时允许延迟注入方法和更改方法实现,因此无论如何它已经经历了一个蹦床。在这种情况下,进行零检查的额外处罚并不重要。

我希望能帮助一点点来解释理由。

欲了解更多信息,请参阅我的blogposts

8

警告:我不是块的专家。

objective-c objects但调用block is not a message,但你仍然可以尝试[block retain]荷兰国际集团一nil块或其他消息。

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

+0

谢谢你,有趣的链接。我知道调用一个块与发送一条消息并不相同,但从概念上讲,如果nil块与nil对象一样宽容,那将会很好。 – zoul 2010-11-10 14:22:42

+0

您可以将类别添加到'__block'类型中......但我不确定。 '#define nilBlock^{}'也可能让你的生活更轻松。 – 2010-11-10 14:25:05

+0

我想过'nilBlock'方法,不幸的是打字遇到了困难 - 为每个块类型创建一个单独的nil值并不是很有趣。 – zoul 2010-11-10 14:30:17

2

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

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

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

马特加洛韦的答案是完美的!很棒的阅读!

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

#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; 

通过这种方式,您可以轻松定义故障预置值。在我的例子'零'。

+1

__VA_ARGS__在.mm文件中产生问题 – BergP 2013-10-22 08:06:49

+0

@PavelKatunin我从来没有测试过 – hfossli 2013-10-22 09:02:04

相关问题