iOS中,如何判断一个 Block 是全局块、栈块还是堆块?
在 iOS 中,Block 有三种基本类型,分别对应不同的存储区域:
全局块(NSGlobalBlock)、
栈块(NSStackBlock)、
堆块(NSMallocBlock)。
区分它们的核心是通过 存储位置、是否捕获外部变量 以及 是否执行过 copy 操作。以下是具体区分方法和细节:
一、Block 类型的本质与分类
1. 全局块(NSGlobalBlock)
• 存储位置:全局静态存储区(程序数据段)。
• 特点:
不捕获任何外部变量(包括全局变量、静态变量)。
生命周期与程序一致,无需内存管理(分配/释放)。
在 ARC 和 MRC 下表现一致,不会被 copy 操作影响(因为本身就在全局区)。
• 示例:
void (^globalBlock)(void) = ^{
NSLog(@"Global Block"); // 未捕获任何局部变量
};
NSLog(@"%@", [globalBlock class]); // 输出:__NSGlobalBlock__(或 NSGlobalBlock)
2. 栈块(NSStackBlock)
• 存储位置:栈区(函数栈帧内)。
• 特点:
捕获了外部自动变量(局部变量,包括 self、成员变量等)。
生命周期受限于所在作用域,超出作用域后会被系统自动释放。
在 MRC 下,栈块不会自动 copy 到堆区,需手动调用 copy 方法;在 ARC 下,某些场景会自动触发 copy(见下文)。
• 示例:
int age = 18;
void (^stackBlock)(void) = ^{
NSLog(@"Age: %d", age); // 捕获了局部变量 age
};
NSLog(@"%@", [stackBlock class]); // 输出:__NSStackBlock__(或 NSStackBlock,ARC 下可能已自动 copy 为堆块,需看上下文)
3. 堆块(NSMallocBlock)
• 存储位置:堆区。
• 特点:
由栈块通过 copy 操作产生(包括 ARC 下的自动 copy)。
生命周期由内存管理机制控制(MRC 下需手动 release,ARC 下自动管理)。
是最常用的 Block 类型(如作为属性、参数传递时)。
• 示例:
int age = 18;
void (^heapBlock)(void) = [^{ // MRC 下显式 copy,ARC 下某些场景隐式 copy
NSLog(@"Age: %d", age);
} copy];
NSLog(@"%@", [heapBlock class]); // 输出:__NSMallocBlock__(或 NSMallocBlock)
二、区分 Block 类型的方法
1. 通过 class 方法打印类型
直接调用 [block class] 输出类名:
• 全局块:__NSGlobalBlock__(或 NSGlobalBlock,iOS 版本可能影响前缀)。
• 栈块:__NSStackBlock__(或 NSStackBlock)。
• 堆块:__NSMallocBlock__(或 NSMallocBlock)。
2. 根据是否捕获外部变量判断
• 无捕获:必定是全局块(无论是否 copy,因为没有需要存储的状态)。
• 有捕获:
在 定义时:若未执行 copy,且在栈作用域内,是栈块(ARC 下可能自动 copy 为堆块,需看使用场景)。
在 作为属性、参数传递或返回值时:ARC 会自动将栈块 copy 为堆块(例如 strong 或 copy 修饰的属性),此时是堆块。
3. 根据内存管理场景判断(ARC vs MRC)
• MRC 下:
栈块:未调用 copy,且在栈作用域内(如函数内定义未传递出去)。
堆块:手动调用 copy(如 [block copy])或作为 copy 修饰的属性值。
• ARC 下:
栈块:仅在定义后未被任何强引用持有,且未超出作用域时短暂存在(极少见,因为 ARC 会自动对需要延长生命周期的栈块执行 copy)。
堆块:几乎所有被强引用持有的 Block(如属性、集合对象中的 Block)都是堆块,因为 ARC 会自动 copy。
三、关键场景中的 Block 类型变化
1. 定义时的默认类型
• 无捕获:全局块(无论 ARC/MRC)。
• 有捕获:
MRC:栈块(需手动 copy 到堆)。
ARC:栈块,但当 Block 被赋值给强引用(如属性、变量)时,ARC 会自动 copy 为堆块。
2. 作为函数参数传递
• 若函数参数类型为 void (^)(void)(非 copy 修饰):
MRC:传递栈块,接收方需手动 copy 以延长生命周期。
ARC:传递时自动 copy 为堆块(底层调用 Block_copy)。
• 若函数参数使用 copy 修饰(如 GCD 的 dispatch_async):会强制将栈块 copy 为堆块。
3. 作为属性修饰符
• strong 修饰:ARC 下会自动 copy 栈块为堆块(等效于 copy,因为 Block 是对象,strong 对 Block 的效果和 copy 一致)。
• copy 修饰(推荐):显式确保 Block 被 copy 到堆区,避免栈块释放后野指针问题。
四、面试常见问题与答案
问题 1:如何判断一个 Block 是全局块、栈块还是堆块?
答案:
通过 [block class] 打印类名:
• 全局块(无捕获):类名为 __NSGlobalBlock__。
• 栈块(有捕获且未 copy):类名为 __NSStackBlock__(仅在 ARC 下短暂存在,或 MRC 未手动 copy 时)。
• 堆块(有捕获且被 copy):类名为 __NSMallocBlock__(ARC 下绝大多数场景如此,如作为属性、参数传递时)。
问题 2:ARC 下,栈块何时会被自动 copy 为堆块?
答案:
以下场景中,ARC 会自动将栈块 copy 到堆区:
1. 当 Block 被赋值给 strong/copy 修饰的属性或变量时。
2. 当 Block 作为参数传递给 GCD 函数(如 dispatch_async)、block_copy 等会触发 copy 的函数时。
3. 当 Block 被添加到集合对象(如 NSArray、NSDictionary)中时。
问题 3:MRC 下,栈块和堆块的内存管理有何区别?
答案:
• 栈块:存储在栈区,生命周期随作用域结束而释放,无需手动释放,也不能调用 retain/release。
• 堆块:通过 copy 操作创建,需手动调用 release(或 autorelease)释放内存,遵循引用计数规则。
问题 4:为什么 Block 属性通常用 copy 修饰?
答案:
• 在 MRC 下,确保栈块被 copy 到堆区,避免栈块作用域结束后被释放,导致野指针。
• 在 ARC 下,copy 和 strong 对 Block 的效果一致(都会自动 copy 栈块),但 copy 更清晰地表达了“确保 Block 存储在堆区”的意图,是更标准的做法。
总结
区分 Block 类型的核心是:
1. 是否捕获外部变量(决定是否为全局块)。
2. 是否执行过 copy 操作(栈块 → 堆块的关键)。
3. 内存管理环境(ARC/MRC)(影响 copy 的自动触发)。
实际开发中,ARC 下绝大多数 Block 都是堆块,只需关注捕获变量导致的循环引用问题;而 MRC 下需手动管理栈块的 copy 和内存释放。通过打印 class 或分析捕获行为,可快速判断 Block 类型。