【问题标题】:Should I still copy/Block_copy the blocks under ARC?我还应该复制/Block_copy ARC 下的块吗?
【发布时间】:2014-04-28 07:20:34
【问题描述】:

我刚刚偶然发现了以下 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?.

【问题讨论】:

  • 块一直是对象。而 OS_OBJECT_USE_OBJC 与块无关。这些是用于调度队列之类的。
  • 谢谢,我已经解决了我的问题。

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


【解决方案1】:

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);
}

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

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

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

【讨论】:

  • "ARC 会自动复制区块。"对,except 仅用作调用的参数。
  • @newacct 作为优化,是的。如果一个块仅用作调用的参数,那么它不会超过其范围,也不需要复制。如果此类参数的接收者以需要保留块的方式使用它,ARC 将再次自动复制该块。
  • @MattStevens:“如果此类参数的接收者以需要保留块的方式使用它,ARC 将再次自动复制块。”如果接收者不知道这是一个块,则不会。
  • 你能举一个不发生块复制的例子吗?
  • @newacct 我采用该优化来引用块指针类型的参数,但我明白你的意思。在实践中,编译器会在您的场景的块参数上发出对 objc_retainBlock() 的调用,因此 ARC 也会自动将块复制到那里。
【解决方案2】:

编辑:

事实证明,检查“捕获”变量的地址很难解释,并且并不总是适合确定块是否已复制到堆中或仍然驻留在堆中。尽管此处给出的块规范BLOCK IMPLEMENTATION SPECIFICATION 足以描述事实,但我尝试了一种完全不同的方法:

什么是块?

这是官方规范BLOCK IMPLEMENTATION SPECIFICATION的总结:

存在代码块(如函数)和一个结构,其中包含多个数据、标志和函数指针的和平以及“捕获的变量”的可变长度部分。

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

一个Block可以定义在函数范围内,这个结构是在堆栈本地内存中创建的,也可以定义在全局或静态范围内,结构是在静态存储中创建的。

一个块可以“导入”其他块引用、其他变量和__block修改的变量。

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

  • 堆栈本地(自动)变量将通过制作“常量副本”来“导入”。

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

  • 全局变量将被简单地引用(而不是“导入”)。

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

由于这个“捕获的变量”是只读的,编译器可以应用一些优化:例如,如果我们需要一个副本,我们真的只需要在堆上一个捕获变量的实例块。

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

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

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

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

根据 Block API,版本 3.5:

最初,当一个块字面量被创建时,这个结构将存在于堆栈中:

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

  1. 一个静态描述符结构的声明和初始化如下:

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

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

    c。如果 Block 字面量需要,则 copy_helper 和 dispose_helper 函数指针将设置为各自的辅助函数。

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

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

    b. flags 字段设置为零,除非有导入到 Block 中的变量需要程序级 Block_copy() 和 Block_release() 操作的辅助函数,在这种情况下设置 (1

请注意,这是针对块文字

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

观察

现在,很难编写测试代码来证明这些断言。因此,使用调试器并在相关函数处设置符号断点似乎更好

  • _Block_copy_internal

  • malloc(只有在命中第一个断点后才应启用)

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

在下面的代码 sn-p 中,我们创建一个 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 处设置了一个断点——但是,它不会被命中。这表明该块尚未被复制到堆上。可以使用 Instruments 进行另一个证明,它不显示函数 foo 中的分配。

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

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

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

在下面的类似代码中使用 Instruments 跟踪的分配

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();
}    

表明,Block 确实会被复制:

值得注意

  • 当一个块不捕获任何变量时,它就像一个普通函数。然后应用优化是有意义的,其中 Block_copy 操作实际上什么都不做,因为没有什么要复制的。 clang 通过使此类块成为“全局块”来实现这一点。

  • 当将copy 发送给一个块变量并将结果分配给另一个块变量时,例如:

    dispatch_block_t block = ^{
        int y0 = capture_me;
    };
    dispatch_block_t otherBlock = [block copy];
    

    copy 将是一个非常便宜 的操作,因为块block 已经为可以共享的块结构分配了存储空间。因此,copy 不需要再次分配存储空间。

回到问题

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

@property (copy) block_t completion

[^{...} copy]

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

如果是块 property,如果我们简单地 wright 应该是安全的:

@property dispatch_block_t completion;

然后:

foo.completion = ^{ x = capture_me; ... };

这应该是安全的,因为分配一个块字面量(位于堆栈上)底层块变量_completion,会将块复制到堆上。

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

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

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

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

其他场景更微妙:

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();
});

但看起来,Block 文字将被正确复制,因为将参数(Block)分配给方法 id 中的参数(id),该方法将 Block 文字复制到堆上:

此外,NSArray保留在方法arrayWithObject: 中作为参数传递的对象。这会导致再次调用 Block_copy()然而,由于 Block 已经在堆上,因此该调用不会分配存储空间。

这表明,在arrayWithObject: 的实现中某处发送到块字面量的“保留”消息也确实会复制块。

那么,我们什么时候真正需要显式复制一个 Block?

一个 块文字 - 例如表达式:

^{...}

将在堆栈上创建块结构。

所以,我们实际上需要只在我们需要堆上的块并且这不是“自动”发生的情况下制作副本。但是,几乎没有出现这种情况的情况。即使我们将 Block 作为参数传递给不知道它是 Block 字面量并且首先需要 copy 的方法(例如:arrayWithObject:),并且接收者可能只发送一个 retain 向参数中给定的对象发送消息,Block 将首先被复制到堆中。

我们可能显式使用copy 的示例是,我们没有将块字面量分配给块变量或id,因此 ARC 无法确定它必须复制块对象还是必须发送“保留”。

在这种情况下,表达式

[^{...} copy];  

将产生一个自动释放的块,其结构驻留在堆上。

【讨论】:

  • 您正在打印堆栈块变量的地址,通过执行&block,而不是块本身。由于块是 Obj-C 对象,因此它们是指针。
  • @coverback 我想,我需要澄清一些事情。您修改后的答案实际上不适合证明任何事情:) 我将编辑可能的答案以明确这一点,请耐心等待:)
  • 同时我对@coverback 的评论 +1。
  • 强调测试代码的关键方面:auto y0, y1, y2, y3 变量的地址仅似乎存在于堆中。这些只是让您感到困惑的类似地址:当您通过 GCD 异步分派到这些线程时,像 0x1000b7ddc 这样的地址是 另一个线程的堆栈帧的局部变量!在 dispatch_asyncs 本身内添加一些局部变量(不在两个块中的任何一个内!),您会看到它产生的数字与堆内存的数字相似 - 但它们只是相似的。它是这些线程的堆栈,而不是堆!
  • @Stanislaw 我已经完全重写了答案并试图澄清这些位。 “捕获”变量 (x0, x1) 的地址并不总是清楚地指示块是否已被复制。块局部变量(y0,y1)的地址是相应线程中的堆栈地址。要确定一个块是否已被复制,实际上需要查看反汇编。在新的答案中,我使用 Instruments 和调试器来跟踪 Block_copy() 和分配。
【解决方案3】:

我原来的答案是错误的。 编辑后的答案不是答案,而更像是“这确实是一个好问题”。

请参阅马特的回答,了解为什么事情是这样的真实参考。

在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。在将堆栈“向下”传递到 arrayWithObjects: 和其他执行保留的方法时,您仍然需要使用 [^{} copy]

And docs about blocks and properties 说:

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

【讨论】:

  • 感谢您的回答,对不起:我将接受以下两种回答:参考2013-2014年的一些权威链接和/或iOS6-7、Mac OSX10上的任何好的崩溃示例。 8-9 个运行时。
  • "虽然没有一个文档明确说明哪些内容发生了变化,哪些内容没有发生变化,但可以从这些位中得出结论,块需要在属性中复制以及何时将它们传递到不同的堆栈范围。”这正是我要澄清的一点!当然,我知道这些官方链接,我只是不能确定它们是否真的反映了当前的情况。
  • @Stanislaw Coverback 是对的。重要的一点是: 1. Block 最初会在堆栈上创建(与“普通”Objective-C 对象相反,后者总是在堆上创建)。 2. 当设置一个值为 Block 的属性时,我们真的需要它在堆上(就像其他“普通”对象一样)。否则,我们将在 ivar 中保留一个悬空指针。其值为可保留指针的属性的设置器的默认行为是“保留它”。因此,我们需要设置属性copy
  • @Stanislaw 注意:使用属性copy 总是安全的,但在块已经在堆上的情况下并不总是必要的。 (当 Block 已经在堆上时,副本实际上只是一个“保留”)。
  • @coverback:没有测试可以证明块不是从堆栈开始的。您的测试结果与从堆栈开始,然后在分配给变量之前由 ARC 复制的块的假设完全一致。 ARC 允许为块任意插入额外的副本。
猜你喜欢
  • 2012-06-30
  • 1970-01-01
  • 2011-02-25
  • 2014-08-06
  • 2011-10-27
  • 1970-01-01
  • 2013-01-30
相关资源
最近更新 更多