iOS中,如何判断一个 Block 是全局块、栈块还是堆块?

2025-05-07  本文已影响0人  博文得礼

在 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 类型。

上一篇 下一篇

猜你喜欢

热点阅读