【问题标题】:Why do NSBlocks have to be copied for storage in containers?为什么必须复制 NSBlock 才能存储在容器中?
【发布时间】:2013-06-27 10:44:47
【问题描述】:
- (void) addABlock 
{
void (^aBlock)(void) = ^() { [someObject doSomething]; };

[self.myMutableArray addObject: aBlock];  // Oops..

[self.myMutableArray addObject: [aBlock copy]];  // works fine
}

在上面的简化示例中,如果未执行块复制,我会看到未定义的行为。此案例在苹果的 ARC 过渡指南中专门列出。

我不明白的部分是为什么我必须手动调用复制。该块是在堆栈上创建的,因此需要执行 block_copy - 这很清楚。 NSArray 不调用复制,但它应该在添加的对象上调用保留。那么为什么 [NSBlock retain] 不直接调用 [NSBlock copy] 呢?

http://developer.apple.com/library/mac/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html

http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/

【问题讨论】:

    标签: objective-c block objective-c-blocks


    【解决方案1】:

    更新

    尽管Apple documentation 说了什么:

    当您在 ARC 模式下将块向上传递到堆栈时,块“正常工作”,例如在返回中。您不必再调用 Block Copy。在将堆栈“向下”传递到 arrayWithObjects: 和其他执行保留的方法时,您仍然需要使用 [^{} copy]

    不再需要在将块添加到容器时手动调用块上的copy。 在这种情况下缺少自动复制已被认为是编译器错误并在很久以前在llvm 中修复。

    “我们认为这是一个编译器错误,它已在开源 clang 存储库中修复了数月。”

    John McCall,LLVM 开发人员)

    我使用最新的 Apple LLVM 5.0 编译器在 Xcode 5 中亲自对此进行了测试。

    - (NSArray *)blocks {
        NSMutableArray *array = [NSMutableArray array];
        for (NSInteger i = 0; i < 3; i++) {
            [array addObject:^{ return i; }];
        }
        return array;
    }
    
    - (void)test {
        for (NSInteger (^block)() in [self blocks]) {
            NSLog(@"%li", block());
        }
    }
    

    上面的例子正确打印

    0
    1
    2
    

    在 ARC 下,它在 MRC 中与 EXC_BAD_ACCESS 一起崩溃。

    请注意,这 - 最后 - 与 llvm documentation 一致,它声明

    当这些语义要求保留块指针类型的值时,它具有Block_copy的效果

    意味着每当 ARC 必须保留一个指针并且该指针恰好是块指针类型时,将调用 Block_copy 而不是 retain


    原答案

    我不明白的部分是为什么我必须手动调用复制。

    块是分配在堆栈上的 Objective-C 对象的少数示例之一(出于性能原因),因此当您从方法调用返回时,由于当前堆栈帧的拆除,您会丢失它们。

    在堆栈块上发送copy 将在其上调用Block_copy 并将其移动到堆上,从而允许您保持对该块的有效引用。

    那么为什么[NSBlock retain] 不直接打电话给[NSBlock copy]

    这会破坏retain 的通常语义,它应该返回对象本身,并增加保留计数。由于在堆栈块上增加保留计数没有任何意义,因此在堆栈块上调用 retain 没有任何效果。

    Apple 可以按照您的建议以不同的方式实现它,但他们更愿意尽可能地遵守内存管理方法的通用合同。

    作为关于块的进一步参考,您可能想看看@bbum 的this great blog post。它是 ARC 之前的版本,但大多数概念没有改变。

    【讨论】:

    • 那么如果我在一个块被复制到堆之前调用retain会发生什么?或者换一种说法,为什么苹果没有像这样实现 NSBlock 保留: - (id) retain { return [self copy]; }
    • 如果块还在堆栈上,它就没有意义,因为它什么也不做。在块上调用copy 将调用Block_copy,它会物理地移动堆上的块。还值得注意的是,额外调用Block_copy(即copy)不会产生该块的其他副本,但它们只会增加保留计数。
    • 此外,这将违反retain的合同。 retain 不应该返回对象的副本,而是返回对象本身。苹果本可以这样做,但他们(在我看来)正确地决定不这样做。
    • 完全正确;如果retain 合约被破坏,就会有大量 代码被破坏。这与 ARC 的承诺(在实施的早期阶段)相结合,指导设计不违反上述合同,因为在 ARC 下,最终在所有情况下都可以自动处理。
    • @bbum 如果你有时间,请看看我的回答,让我知道我是否遗漏了什么。谢谢!
    【解决方案2】:

    在 ARC 下,在这种情况下或在大多数其他情况下,您不再需要手动复制块。根据clang's ARC documentation on blocks

    除了作为初始化 __strong 参数变量或读取 __weak 变量的一部分完成的保留之外,只要这些语义要求保留块指针类型的值,它就会具有 Block_copy 的效果。当优化器发现结果仅用作调用的参数时,它可能会删除此类副本。

    换句话说,大多数情况下,保留块具有 Block_copy 的效果,正如您所建议的那样。特别是,当块被添加到集合中时,它会被复制! (更准确地说,它已经在堆上。)下面是一些示例代码,证明了这种情况。

    #import <Foundation/Foundation.h>
    
    int main(int argc, char *argv[]) 
    {
        @autoreleasepool 
        {
            NSMutableArray *arr = [[NSMutableArray alloc] init];
            int counter = 0;
            int total = 5;
            for (int i = 0; i < total; i++) {
                void (^block)(void) = ^{
                    NSLog(@"in this block, counter is %d", counter);
                };
                [arr addObject:block];
                counter += 1;
            }
    
            for (int i = 0; i < total; i++) {
                void (^block)(void) = arr[i];
                block();
            }
        }
    }
    

    如果使用默认编译器(Apple LLVM 编译器 4.2)在最新的 Xcode (4.6.3 (4H1503)) 上运行,输出将是

    in this block, counter is 0
    in this block, counter is 1
    in this block, counter is 2
    in this block, counter is 3
    in this block, counter is 4
    

    如果这些块没有被复制到堆中,你会(可能——这是未定义的行为)看到

    in this block, counter is 4
    in this block, counter is 4
    in this block, counter is 4
    in this block, counter is 4
    in this block, counter is 4
    

    因为添加到数组中的指针都指向堆栈分配(非复制)块,该块在执行块时捕获了counter4

    尤其是,如果您禁用 ARC,您将获得相同的行为。即使您在将块添加到数组之前在块上调用retain

    [arr addObject:[block retain]];
    

    您仍然会得到相同的(“损坏的”)输出,说明这是一种 ARC 行为,而不是一般的保留行为

    注意:

    ARC的retain没有Block_copy作用的两个地方是

    (1) 作为初始化__strong 参数变量的一部分保留完成

    在进入一个函数(或方法)后,如果一个对象被传递给该函数(或方法),该对象将被保留(堆栈中有对该对象的强引用)框架)并在函数(或方法)退出时释放(对对象的强引用被释放)。

    这对于方块和任何其他对象一样都是正确的。 clang 文档中的这句话意味着即使在这种情况下保留了该块,该块也不会被此保留复制。

    (2) 作为读取__weak 变量的一部分保留完成

    类似地,当__weak 变量被读入__strong 变量时,会创建对该对象的强引用,并保留该对象。

    块也是如此。但是,以这种方式发送的块不会被复制(由于将__weak 引用读入__strong 引用)。

    这两个异常中的任何一个都会给您带来问题的情况很少见。一般来说,您不必担心复制块。

    【讨论】:

    • 除非我没有很好地获得文档,否则您的粗体陈述通常是错误的(正如您自己在后面的答案中指出的那样)。保留一个块不会做任何事情。但是,您有一个观点:显然在某些情况下您希望 ARC 保留该块,而是执行 Block_Copy。最重要的是,在仍在堆栈上的块上调用 retain 仍然毫无意义。不过答案很好。
    • 引用 Apple's documentation: 当你在 ARC 模式下将块向上传递到堆栈时,块“正常工作”,例如在返回中。您不必再调用 Block Copy。 在将堆栈“向下”传递到 arrayWithObjects: 和其他执行保留的方法时,您仍然需要使用 [^{} copy]。
    • 我的印象是 Apple 的某些文档可能与 clang 的文档不是最新的。
    • 在我的特定测试用例中,我创建了一个块文字,将其作为参数传递给函数,并且该函数尝试将参数添加到 NSArray。在这种情况下绝对没有自动 block_copy。 (XCode 4.6.3,运行时 iOS 5.0)
    • "特别是,当块被添加到集合中时,它被复制了!"不必要。您自己引用了这句话:“当优化器看到结果仅用作调用的参数时,它可能会删除此类副本。”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多