在 ARC(自动引用计数)环境下,编译器为了保证程序的安全性,会非常频繁地自动将 Block 从栈(Stack)拷贝到堆(Heap)上。一旦拷贝到堆,Block 的生命周期就不再受限于当前作用域,而是由引用计数管理。
以下是触发 Copy 操作的三个核心时机:
1. 将 Block 赋值给强引用对象
这是最常见的场景。当你将一个 Block 赋值给一个 __strong 修饰的变量(默认就是 __strong)或者类的一个属性(Property)时,编译器会自动调用 copy。
Objective-C
// 这里的 block 变量默认是 __strong,会触发 copy
void (^myBlock)(void) = ^{
NSLog(@"Hello World");
};
2. 作为函数的返回值(Return)
在 MRC 时代,如果函数返回一个栈上的 Block,外部调用者拿到的是一个瞬间销毁的指针。在 ARC 下,编译器会检测到 Block 作为返回值,并自动执行 copy,确保调用方拿到的是存储在堆上的安全副本。
Objective-C
typedef void(^Handler)(void);
Handler getBlock() {
int x = 10;
// 虽然这里看起来是在栈上创建,但 return 时会自动 copy 到堆
return ^{ NSLog(@"%d", x); };
}
3. 作为某些系统 API 的参数
Cocoa Touch 框架中,很多带有 usingBlock: 字样的方法,或者 GCD(Grand Central Dispatch)的方法,内部都会对传入的 Block 进行 copy 以保证异步执行时的安全性。
- GCD 方法: 例如
dispatch_async(queue, block); - 集合遍历: 例如
[array enumerateObjectsUsingBlock:...]
补充时机:__block 变量的作用
虽然这不直接是 Block 的 copy,但息息相关:当一个 Block 被拷贝到堆时,它所捕获的由 __block 修饰的变量也会被从栈拷贝到堆上,以确保变量和 Block 的生命周期同步。
总结对照
| 动作 | 内存变化 |
|---|---|
| 定义在方法内且未赋值 | _NSConcreteStackBlock (栈) |
执行 [block copy] 或强引用赋值 | _NSConcreteMallocBlock (堆) |
| 未捕获外部变量 (或只捕获全局变量) | _NSConcreteGlobalBlock (全局区) |