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

Sta*_*ich 22 objective-c objective-c-blocks automatic-ref-counting

我刚刚偶然发现了以下SO主题:我们为什么要复制块而不是保留?其中包含以下句子:

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

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

@property (copy) blockProperties 要么

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

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

我希望我所做的描述很清楚.

请详细说明.


类似的问题

在ARC下,当直接分配给ivar时,是否会自动复制块?.

Mat*_*ens 40

ARC将自动复制块.来自clang的Objective-C自动参考计数文档:

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

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

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}
Run Code Online (Sandbox Code Playgroud)

将块属性声明为具有复制语义仍然是一个好主意,因为实际上会复制分配给强属性的块.Apple也建议这样做:

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

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

  • “ ARC将自动复制该块。” 是的,* except *仅在用作呼叫参数时使用。 (2认同)
  • @newacct作为优化,是的.如果块仅用作调用的参数,则它不会超出其范围,也不需要复制.如果这种参数的接收者以需要保留块的方式使用它,ARC将再次自动复制块. (2认同)
  • 你能展示一个没有发生块复制的例子吗? (2认同)
  • @newacct我把这个优化引用到块指针类型的参数,但我明白你的意思了.实际上,编译器会针对您的场景在块参数上调用`objc_retainBlock()`,因此ARC也会在那里自动复制块. (2认同)

Cou*_*per 6

编辑:

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

无论如何,Block是什么?

这是官方规范块实施规范的摘要:

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

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

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

块可以"导入"其他块引用,其他变量和__block修改的变量.

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

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

  • __block改性变量将通过分配的指针变量封闭在另一结构的地址来导入.

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

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

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

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

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

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

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

根据Block API,版本3.5:

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

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

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

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

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

    C.如果Block文本需要copy_helper和dispose_helper函数指针,则将它们设置为各自的辅助函数.

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

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

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

请注意,这适用于块文字.

根据Objective-C Extensions to Blocks,编译器会将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);
    });
}  
Run Code Online (Sandbox Code Playgroud)

输出如下:

Hello block 1
Address of auto y0: 0x7fff5fbff8dc
Address of captured x0: 0x7fff5fbff940
Run Code Online (Sandbox Code Playgroud)

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

我们也设置了一个断点_Block_copy_internal- 但是,它不会被击中.这表明Block尚未复制到堆上.可以使用仪器进行另一个证明,它不显示功能分配foo.

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

int capture_me = 1;
dispatch_block_t block = ^{ int y = capture_me; };
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

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

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

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();
}    
Run Code Online (Sandbox Code Playgroud)

显示,Block将被复制:

在此输入图像描述

值得注意的

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

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

    dispatch_block_t block = ^{
        int y0 = capture_me;
    };
    dispatch_block_t otherBlock = [block copy];
    
    Run Code Online (Sandbox Code Playgroud)

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

回到问题

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

@property (copy) block_t completion

[^{...} copy]
Run Code Online (Sandbox Code Playgroud)

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

在块属性的情况下,如果我们简单地怀疑它应该是安全的:

@property dispatch_block_t completion;
Run Code Online (Sandbox Code Playgroud)

然后:

foo.completion = ^{ x = capture_me; ... };
Run Code Online (Sandbox Code Playgroud)

这应该是安全的,因为分配块文字(它存在于堆栈中)的底层块变量_completion,会将块复制到堆上.

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

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

dispatch_async(queue, ^{int x = capture_me;});
Run Code Online (Sandbox Code Playgroud)

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

其他情况更微妙:

dispatch_block_t block;
if (condition) {
    block = ^{ ... };
}
else {
    block = ^{ ... };
}
dispatch_sync(queue, block);
Run Code Online (Sandbox Code Playgroud)

但实际上,这是安全的:块文字将被复制并分配块变量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();
});
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述

此外,NSArray保留作为方法中的参数传递的对象arrayWithObject:.这会导致另一个调用Block_copy(),因为Block已经在堆上,此调用不会分配存储.

这表明,在执行过程中某处发送给Block文字的"保留"消息arrayWithObject:也确实会复制该块.

那么,我们什么时候才真正需要明确复制一个Block?

字面块 -例如,表达式:

^{...}
Run Code Online (Sandbox Code Playgroud)

将在堆栈上创建块结构.

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

我们可能明确使用的示例copy是,我们没有为块文件分配块变量或者一个id,因此ARC无法弄清楚它必须复制块对象或必须发送"retain".

在这种情况下,表达式

[^{...} copy];  
Run Code Online (Sandbox Code Playgroud)

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