代码之家  ›  专栏  ›  技术社区  ›  zoul

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

  •  72
  • zoul  · 技术社区  · 14 年前

    我开始使用A_地块,很快注意到没有地块会导致公交车错误:

    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
    

    还有别的选择吗?为什么零块不能仅仅是一个nop?

    4 回复  |  直到 9 年前
        1
  •  139
  •   SayeedHussain    10 年前

    我想再详细解释一下,给出一个更完整的答案。首先,让我们考虑下面的代码:

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

    如果你运行这个,你会看到 block() 看起来像这样的行(在32位体系结构上运行时-这很重要):

    exc_bad_访问(代码=2,地址=0xC)

    那么,为什么呢?嗯,那个 0xc 是最重要的一点。崩溃意味着处理器试图读取内存地址的信息。 小精灵 . 这几乎是完全错误的。那里不太可能有任何东西。但它为什么要读取这个内存位置呢?嗯,这是因为一个街区实际上是在引擎盖下建造的。

    定义块时,编译器实际上在堆栈上创建一个结构,其形式如下:

    struct Block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct Block_descriptor *descriptor;
        /* Imported variables. */
    };
    

    然后该块就是指向此结构的指针。第四名成员, invoke ,这个结构是有趣的。它是一个函数指针,指向保存块实现的代码。因此,当一个块被调用时,处理器试图跳转到该代码。请注意,如果在 援引 成员,您会发现十进制中有12个,十六进制中有C个。

    因此,当调用一个块时,处理器获取该块的地址,添加12,并尝试加载保存在该内存地址的值。然后它试图跳到那个地址。但如果块为零,它将尝试读取地址 小精灵 . 很明显,这是一个无效地址,因此我们得到了分段错误。

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

    我希望这有助于解释这一理论的基本原理。

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

        2
  •  39
  •   hfossli    9 年前

    马特·盖洛韦的回答是完美的!读得好!

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

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

        3
  •  8
  •   Stephen Furlani    14 年前

    警告:我不是方块专家。

    阻碍 objective-c objects 但是打电话给 block is not a message 尽管你仍然可以尝试 [block retain] A型 nil 阻止或其他消息。

    希望这(以及链接)能有所帮助。

        4
  •  2
  •   Renetik    13 年前

    这是我最简单、最好的解决方案也许有可能用这些c var参数编写一个通用的run函数,但我不知道如何编写它。

    void run(void (^block)()) {
        if (block)block();
    }
    
    void runWith(void (^block)(id), id value) {
        if (block)block(value);
    }
    
    推荐文章