Objective-C 中 Block 有哪几种类型?
在 Objective-C 中,Block 本质上是一个 Objective-C 对象。根据 Block 在内存中的位置以及它是否捕获了外部变量,Block 主要分为三种类型:
__NSGlobalBlock__(全局 Block)__NSStackBlock__(栈 Block)__NSMallocBlock__(堆 Block)
下面详细解释这三种类型及其产生条件:
1. __NSGlobalBlock__ (全局 Block)
- 内存位置:数据区(Data Segment / .data 区),类似于全局变量。
- 产生条件:
- Block 内部没有访问任何外部的局部变量(自动变量)。
- 或者访问的是全局变量、静态变量(static)。
- 生命周期:与应用程序的生命周期相同,程序结束时才释放。
- 特点:对它进行
copy操作无效,仍然是全局 Block。
代码示例:
plaintext
void (^globalBlock)(void) = ^{
NSLog(@"Hello World");
};
NSLog(@"%@", [globalBlock class]); // 输出: __NSGlobalBlock__
2. __NSStackBlock__ (栈 Block)
- 内存位置:栈区(Stack)。
- 产生条件:
- Block 内部访问了外部的局部变量(自动变量)。
- 且 该 Block 没有被强引用(在 ARC 下情况比较特殊,见下文)。
- 生命周期:由系统自动管理。作用域结束(大括号结束)时,Block 就会被销毁。
- 特点:
- 如果 Block 在栈上,一旦作用域结束,Block 内存被回收。如果此时再调用该 Block,会导致 Crash(野指针)。
- 为了解决这个问题,通常需要将其
copy到堆上。
注意(ARC vs MRC):
- 在 MRC (Manual Reference Counting) 下,访问了外部局部变量的 Block 默认就是栈 Block。
- 在 ARC (Automatic Reference Counting) 下,编译器非常智能。只要你把 Block 赋值给一个强引用(Strong)变量,或者作为返回值返回,编译器会自动帮你执行
copy操作,将其变为堆 Block。 - 在 ARC 下要看到栈 Block,通常是在打印时直接创建 Block 而不赋值,或者使用
__weak修饰。
代码示例 (ARC 环境):
plaintext
int age = 10;
// 直接打印,没有赋值给强引用变量
NSLog(@"%@", [^{
NSLog(@"%d", age);
} class]); // 输出: __NSStackBlock__
3. __NSMallocBlock__ (堆 Block)
- 内存位置:堆区(Heap)。
- 产生条件:
- 当一个
__NSStackBlock__被执行了copy操作后,就会变成__NSMallocBlock__。 - 在 ARC 中,当 Block 捕获了外部变量并赋值给
__strong修饰的变量(或属性)时,系统自动将其拷贝到堆上。
- 当一个
- 生命周期:由引用计数管理(Reference Counting)。当引用计数为 0 时销毁。
- 特点:这是我们在开发中最常接触到的 Block 类型,因为我们需要 Block 在作用域结束后依然存在(例如网络回调、按钮点击事件)。
代码示例 (ARC 环境):
plaintext
int age = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"%d", age);
};
// ARC下,赋值给变量会自动 copy 到堆上
NSLog(@"%@", [mallocBlock class]); // 输出: __NSMallocBlock__
总结对比表
| Block 类型 | 类名 | 存储区域 | 捕获局部变量 | Copy 操作结果 |
|---|---|---|---|---|
| 全局 Block | __NSGlobalBlock__ |
数据区 (.data) | 否 | 什么也不做 (仍是 Global) |
| 栈 Block | __NSStackBlock__ |
栈区 (Stack) | 是 | 复制到堆区 (变成 Malloc) |
| 堆 Block | __NSMallocBlock__ |
堆区 (Heap) | 是 (源自栈) | 增加引用计数 |
为什么需要 copy?
在 MRC 时代,Block 默认在栈上。如果 Block 是在一个函数内部定义的,并且作为返回值返回,或者被异步线程持有,当函数执行完毕,栈帧销毁,Block 的内存也就销毁了。此时再去调用 Block 就会崩溃。
执行 copy 操作可以将 Block 的内容(包括它捕获的变量值)从栈复制到堆上。堆上的内存由程序员(或 ARC)管理,从而保证 Block 在作用域结束后依然存活。