3
- (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/

4

2 回答 2

14

更新

尽管Apple文档说:

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

将块添加到容器时不再需要手动调用它。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

EXC_BAD_ACCESS在 ARC 下,它在 MRC 中崩溃。

请注意,这最终与 llvm文档一致,其中指出

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

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


原始答案

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

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

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

那么为什么[NSBlock retain]不简单地打电话给[NSBlock copy]

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

正如您所建议的,Apple 可以以不同的方式实现它,但他们更愿意尽可能地坚持内存管理方法的通用合同。

作为对块的进一步参考,您可能想看看@bbum 的这篇很棒的博客文章。它是 ARC 之前的版本,但大多数概念没有改变。

于 2013-06-29T22:47:52.573 回答
1

在 ARC下,在这种情况下或在大多数其他情况下,您不再需要手动复制块。根据clang 的关于块的 ARC 文档

除了作为初始化 __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) 保留 done 作为初始化__strong参数变量的一部分

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

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

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

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

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

这两个异常中的任何一个都会导致您出现问题的情况很少见。通常,您无需担心复制块。

于 2013-07-01T18:58:50.420 回答