编辑:
事实证明,检查“捕获”变量的地址很难解释,并且并不总是适合确定块是否已复制到堆中或仍然驻留在堆中。尽管此处给出的块规范BLOCK IMPLEMENTATION SPECIFICATION 足以描述事实,但我尝试了一种完全不同的方法:
什么是块?
这是官方规范BLOCK IMPLEMENTATION SPECIFICATION的总结:
存在代码块(如函数)和一个结构,其中包含多个数据、标志和函数指针的和平以及“捕获的变量”的可变长度部分。
请注意,此结构是私有的,并由实现定义。
一个Block可以定义在函数范围内,这个结构是在堆栈本地内存中创建的,也可以定义在全局或静态范围内,结构是在静态存储中创建的。
一个块可以“导入”其他块引用、其他变量和__block修改的变量。
当 Block 引用其他变量时,它们将被导入:
如果要导入变量,则“捕获的变量”位于上述可变长度部分的结构中。也就是说,自动变量(位于块外部)的“对应物”在块结构中具有存储空间。
由于这个“捕获的变量”是只读的,编译器可以应用一些优化:例如,如果我们需要一个副本,我们真的只需要在堆上一个捕获变量的实例块。
在评估块文字表达式时,将设置捕获变量的值。这也意味着,捕获的变量的存储必须是可写的(即没有代码段)。
捕获变量的生命周期是一个函数的生命周期:该块的每次调用都需要这些变量的新副本。
当程序离开该块的复合语句时,位于堆栈中的块中捕获的变量将被销毁。
当块被销毁时,在堆上的块中捕获的变量将被销毁。
根据 Block API,版本 3.5:
最初,当一个块字面量被创建时,这个结构将存在于堆栈中:
当块文字表达式被评估时,基于堆栈的结构被初始化如下:
-
一个静态描述符结构的声明和初始化如下:
一个。调用函数指针设置为一个函数,该函数将 Block 结构作为其第一个参数,并将其余参数(如果有)传递给 Block,并执行 Block 复合语句。
b. size 字段设置为以下 Block 文字结构的大小。
c。如果 Block 字面量需要,则 copy_helper 和 dispose_helper 函数指针将设置为各自的辅助函数。
-
一个堆栈(或全局)块字面量数据结构的创建和初始化如下:
一个。 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];
将产生一个自动释放的块,其结构驻留在堆上。