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


73

我开始大量使用块,不久便注意到零块会导致总线错误:

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个块不能仅仅是点头的原因?

Answers:


144

我想用一个更完整的答案来进一步解释这一点。首先让我们考虑以下代码:

#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,然后尝试加载该内存地址中保存的值。然后,它尝试跳转到该地址。但是,如果该块为nil,它将尝试读取该地址0xc。显然,这是达芙地址,因此我们得到了分段错误。

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

我希望这可以帮助您解释其基本原理。

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


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”。


1
VA_ARGS在.mm文件中出现问题
BergP 2013年

9

警告:我不是Blocks方面的专家。

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

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


谢谢,有趣的链接。我知道调用一个块与向其发送消息并不相同,但是从概念上讲,如果零个块与零个对象一样宽容,那就太好了。
zoul 2010年

您也许可以在__block类型中添加一个类别...但是我不确定。 #define nilBlock ^{}也可能使您的生活更轻松。
Stephen Furlani 2010年

我考虑过这种nilBlock方法,不幸的是打字方式很麻烦–为每种块类型创建一个单独的nil值并不是一件很有趣的事情。
zoul 2010年

我不知道是否可以继承Blocks,但是添加一条[block call]进行内部检查的消息可能会有所帮助。不确定与ObjC对象的接近程度。
Stephen Furlani 2010年

3
我一般只是做块?block():无; 这对我来说足够简洁,并且在您的工作中透明...
Patrick Pijnappel 2012年

2

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

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

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

这是:块吗?block():无; 更好?
Tibin Thomas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.