我还应该复制/ Block_copy ARC下的块吗?

时间:2014-04-28 07:20:35

标签: objective-c automatic-ref-counting objective-c-blocks

我偶然发现了以下SO主题:Why should we copy blocks rather than retain?,其中包含以下句子:

  

但是,从iOS 6开始,它们被视为常规对象,因此您无需担心。

我真的对这个断言感到困惑,这就是为什么我要问:这个断言是否真的暗示Objective-C开发人员不需要

@property (copy) blockProperties

[^(...){...) {} copy]

将块及其内容从堆栈复制到堆中了吗?

我希望我所做的描述很明确。

请详细说明。


类似问题

Under ARC, are Blocks automatically copied when assigned to an ivar directly?

3 个答案:

答案 0 :(得分:39)

ARC将自动复制该块。来自clang的Objective-C Automatic Reference Counting文档:

  

除了作为初始化__strong参数变量或读取__weak变量的一部分完成的保留,每当这些语义要求保留块指针类型的值时,它具有以下效果:一个Block_copy。当优化器看到结果仅用作调用的参数时,可以删除此类副本。

因此,仅用作函数或方法调用的参数的块可能仍然是堆栈块,但是除非ARC保留块,否则它将复制块。这是由编译器发出对objc_retainBlock()的调用来实现的,implementation的调用是:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

将块属性声明为具有复制语义仍然是一个好主意,因为实际上会复制分配给强属性的块。 Apple recommends this也是如此:

  

您应该将copy指定为属性属性,因为需要复制块以跟踪其在原始范围之外的捕获状态。这不是您在使用自动引用计数时需要担心的事情,因为它会自动发生,但是属性属性的最佳实践是显示结果行为。

请注意,由于ARC提供此保留时复制功能,因此它仅依赖于ARC或ARCLite可用性,并且不需要特定的操作系统版本或OS_OBJECT_USE_OBJC

答案 1 :(得分:6)

编辑:

原来检查了"捕获"的地址。变量很难解释,并不总是适合确定块是否已复制到堆中或仍然驻留在堆上。虽然这里给出的块的规范BLOCK IMPLEMENTATION SPECIFICATION将充分描述事实,但我尝试了一种完全不同的方法:

无论如何,Block是什么?

这是官方规范BLOCK IMPLEMENTATION SPECIFICATION的摘要:

存在一个代码块(如一个函数)和一个包含几个数据和,数据和标志和函数指针的结构,以及一个用于"捕获变量"的可变长度部分。

请注意,此结构是私有的,并且已实现定义。

可以在函数范围中定义块,其中此结构在堆栈本地内存中创建,或者可以在全局或静态范围中定义,其中结构在静态存储中创建。

A Block可以" import"其他块引用,其他变量和__block修改后的变量。

当Block引用其他变量时,它们将被导入:

  • 堆栈本地(自动)变量,将被"导入"通过制作" const副本"。

  • 将通过指定包含在另一个结构中的该变量的地址的指针来导入__block修改过的变量。

  • 全局变量将被简单引用(不是"导入")。

如果要导入变量,则捕获变量"在可变长度部分中存在上述结构。也就是说,"对应物"自动变量(位于块外部)的块在块的结构中具有存储。

因为这个"捕获的变量"只读,编译器可以应用一些优化:例如,如果我们需要块的副本,我们实际上只需要堆上捕获变量的一个实例。

将在计算块文字表达式时设置捕获变量的值。这也意味着,捕获变量的存储必须是可写的(即没有代码段)。

捕获变量的生命周期是函数的生命周期:每次调用该块都需要这些变量的新副本。

当程序离开块的复合语句时,销毁堆栈中的块中的捕获变量将被销毁。

当块被销毁时,生存在堆上的块中的捕获变量将被销毁。

根据Block API,版本3.5:

最初,当创建块文字时,此结构将存在于堆栈中:

  

评估块文字表达式时,基于堆栈的结构初始化如下:

     
      
  1. 声明并初始化静态描述符结构如下:

         

    一个。调用函数指针设置为一个函数,该函数将Block结构作为其第一个参数,将其余参数(如果有)作为Block并执行Block复合语句。

         

    湾size字段设置为以下Block文字结构的大小。

         

    ℃。如果块文字需要它们,则copy_helper和dispose_helper函数指针被设置为各自的辅助函数。

  2.   
  3. 堆栈(或全局)块文字数据结构的创建和初始化如下:

         

    一个。 isa字段设置为外部_NSConcreteStackBlock的地址,它是libSystem中提供的未初始化内存块,如果是静态或文件级别块文字,则为_NSConcreteGlobalBlock。

         

    湾除非有导入块的变量需要帮助程序级别的Block_copy()和Block_release()操作,否则flags字段被设置为零,在这种情况下,设置(1<< 25)标志位。

  4.   

请注意,这适用于阻止文字

根据Objective-C Extensions to Blocks,编译器会将块视为对象

观察

现在,很难制作证明这些断言的测试代码。因此,似乎最好使用调试器并在相关函数中设置符号断点

  • _Block_copy_internal

  • malloc(只有在第一个断点被击中后才能启用)

然后运行合适的测试代码(如下面的代码段)并检查会发生什么:

在下面的代码片段中,我们创建一个Block文字并将其作为参数传递给调用它的函数:

typedef void (^block_t)(void);

void func(block_t block) {
    if (block) {
        block();
    }
}

void foo(int param)
{
    int x0 = param;
    func(^{
        int y0 = x0;
        printf("Hello block 1\n");
        printf("Address of auto y0: %p\n", &y0);
        printf("Address of captured x0: %p\n", &x0);
    });
}  

输出如下:

Hello block 1
Address of auto y0: 0x7fff5fbff8dc
Address of captured x0: 0x7fff5fbff940

"捕获"的地址变量x0强烈表明它存在于堆栈中。

我们还在_Block_copy_internal设置了一个断点 - 然而,它不会被击中。这表明Block尚未复制到堆上。可以使用仪器进行另一个证明,它不会在函数foo中显示分配。

现在,如果我们创建并初始化一个块变量,看起来,最初在堆栈上创建的原始块文字的块数据结构将会被复制到堆上:

int capture_me = 1;
dispatch_block_t block = ^{ int y = capture_me; };

enter image description here

上面复制最初在堆栈上创建的块。这可能只是由于ARC和我们在右边有一个块文字这一事实,而左边的块变量block将被赋予块文字 - 这导致{{1}操作。这使得块看起来很像普通的Objective-C对象。

以下类似代码中的仪器跟踪分配

Block_copy

显示,Block将被复制:

enter image description here

值得注意的

  • 当一个块没有捕获任何变量时,它就像一个普通的函数。然后应用优化是有意义的,其中void foo(int param) { dispatch_queue_t queue = dispatch_queue_create("queue", 0); int x0 = param; dispatch_block_t block = ^{ int y0 = x0; printf("Hello block 1\n"); printf("Address of auto y0: %p\n", &y0); printf("Address of captured x0: %p\n", &x0); }; block(); } 操作实际上什么也没做,因为没有要复制的内容。 clang通过将这些块设为"全局块"。

  • 来实现这一点
  • Block_copy发送到块变量并将结果分配给另一个块变量时,例如:

    copy

    dispatch_block_t block = ^{ int y0 = capture_me; }; dispatch_block_t otherBlock = [block copy]; 将是一个非常便宜的操作,因为块copy已经为可以共享的块结构分配了存储空间。因此,block不需要再次分配存储空间。

回到问题

如果我们需要在某些情况下明确复制块,请回答这个问题,例如:

copy

好吧,一旦块被复制,无论何时,然后将被分配目标变量(块变量) - 我们应该始终安全,而不显式复制块,因为它已经在堆中。 / p>

如果阻止属性,如果我们只是怀疑它应该是安全的:

@property (copy) block_t completion

[^{...} copy]

然后:

@property dispatch_block_t completion;

这应该是安全的,因为分配块文字(存在于堆栈中)的底层块变量foo.completion = ^{ x = capture_me; ... }; ,将块复制到堆上。

尽管如此,我仍然建议使用属性_completion - 因为官方文档仍然建议将其作为最佳实践,并且还支持旧版API,其中Blocks不像普通的Objective-C对象那样运行哪里没有ARC。

如果需要,现有系统API还将负责复制块:

copy

dispatch_async()将为我们制作副本。所以,我们不必担心。

其他情况更为微妙:

dispatch_async(queue, ^{int x = capture_me;});

但实际上,这是安全的:块文字将被复制并分配块变量dispatch_block_t block; if (condition) { block = ^{ ... }; } else { block = ^{ ... }; } dispatch_sync(queue, block);

这个例子看起来甚至可怕:

block

但看起来,块文字将被正确复制为将参数(块)分配给方法int x0 = param; NSArray* array = [NSArray arrayWithObject:^{ int y0 = x0; printf("Hello block 1\n"); }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{ block_t block = array[0]; block(); }); 中的参数(id)的效果,该方法将块文字复制到堆:

enter image description here

此外,arrayWithObject:保留作为方法NSArray中的参数传递的对象。这会导致另一次调用arrayWithObject:,但由于Block已经在堆上,因此该调用不会分配存储。

这表明,"保留"在Block_copy()的实现中发送到块文字的消息也确实会复制块。

那么,我们何时需要明确复制一个Block?

块文字 - 例如表达式:

arrayWithObject:

将在堆栈上创建块结构。

所以,我们实际上需要只在我们需要堆上的Block以及什么时候不会发生"自动"的情况下制作副本。但是,实际上并没有这种情况。即使在我们将Block作为参数传递给方法的情况下,该方法也不知道它是块文字并且首先需要^{...} (例如:copy),并且接收器可能只发送一个保留消息到参数中给出的对象,Block将首先复制到堆上。

我们可能明确使用arrayWithObject:的示例是我们没有为块文件分配块变量或copy,因此ARC无法弄清楚它必须复制块对象或必须发送"保留"。

在这种情况下,表达式

id

将产生一个自动释放的块,其结构位于堆上。

答案 2 :(得分:1)

我的原始回答是错误的。 编辑的答案不是答案,而是更多的“它确实是一个好问题”的事情。

请参阅Matt的答案,了解真实情况,为什么会有这样的东西。

在iOS7上测试以下代码后:

int stackVar;
void (^someBlock)() = ^(){};
NSLog(@"int: %x\nblock: %x,\ncopied: %x", (unsigned int)&stackVar, (unsigned int)someBlock, (unsigned int)[someBlock copy]);

我明白了:

int: bfffda70
block: 9d9948
copied: 9d9948

这几乎意味着创建并分配到堆栈变量中的块实际上已经在堆上并且复制它们不会影响任何内容。

但是,这并没有任何官方来源支持,因为他们仍然声明在堆栈上创建块需要在“传递时”复制。


在我测试之前的部分答案,说明哪些文档与示例相矛盾。

Document about transition to ARC州:

  

在ARC模式下将块传递到堆栈时阻止“正常工作”,例如在返回中。您不必再致电Block_copy了。将堆栈“向下”传递到[^{} copy]以及执行保留的其他方法时,仍需要使用arrayWithObjects:

And docs about blocks and properties说:

  

您应指定copy作为属性属性,因为需要复制块以跟踪其在原始范围之外的捕获状态。