本文专门介绍 Objective-C Block 与 Swift 闭包 的内存管理:Block 的三种类型(全局/栈/堆)、捕获变量与内存、copy 语义、循环引用 与破除,以及作为属性/参数时的注意点。前置知识见 04-ARC详解、06-weak与循环引用。
一、Block 是什么(与内存的关系)
- Block 是 Apple 对 C 语言扩展的闭包:可捕获外部变量、作为对象参与引用计数;在内存上既包含代码(函数指针),也包含捕获的变量(结构体形式),因此既有「存在位置」(栈/堆/全局)也有「对捕获对象的持有关系」。
- 内存管理 需关注两点:Block 对象本身 的分配与释放(栈 block / 堆 block / 全局 block),以及 Block 对捕获变量(尤其是 OC 对象) 的强引用/弱引用,避免循环引用与泄漏。
二、Block 的三种类型与内存位置
2.1 类型与存储位置
| 类型(运行时 isa) | 存储位置 | 产生条件(典型) |
|---|
| NSGlobalBlock | 全局区(.data/.text) | 未捕获任何外部变量(或仅捕获全局/静态变量) |
| NSStackBlock | 栈 | 捕获了自动变量(局部变量),且未 copy 到堆(MRC 下常见) |
| NSMallocBlock | 堆 | 对栈 block 执行 copy,或 ARC 下多数「需要逃逸」的 block 被编译器自动 copy 到堆 |
- 全局 Block:不依赖栈帧,无需 copy,可当作单例使用。
- 栈 Block:随栈帧销毁而失效,若要在作用域外使用(如存为属性、异步回调),必须先 copy 到堆;ARC 下编译器会在赋值给 strong/copy 属性、跨函数传递等场景自动插入 copy。
- 堆 Block:参与引用计数,由 ARC/MRC 管理;copy 时引用计数 +1,release 时 -1。
2.2 简单判断示例(ARC)
void (^gBlock)(void) = ^{ NSLog(@"no capture"); };
int a = 1;
void (^sBlock)(void) = ^{ NSLog(@"%d", a); };
2.3 MRC 下 Block 的 copy 必要性
- 在 MRC 下,栈上的 Block 在函数返回后栈帧被回收,若此时 block 已被传给调用方或存到堆对象(如属性),再执行会野指针/未定义行为。
- 因此 MRC 下:凡是需要跨作用域保留的 block,必须对其执行一次 copy,将栈 block 拷贝到堆上,得到 NSMallocBlock,之后按普通 OC 对象做 retain/release;用完后要对堆 block 做 release(或 autorelease)。
- ARC 下:编译器在「赋值给 strong/copy 属性、作为参数传给会保留 block 的 API」等场景自动插入 copy,一般无需手写
[block copy]。
三、Block 捕获变量与内存
3.1 捕获方式概览
| 捕获对象/变量 | 默认行为(OC 对象) | 对引用计数的影响 |
|---|
| 局部 OC 对象(自动变量) | 强引用(strong) | Block 被 copy 到堆时,会 retain 被捕获的对象;block 释放时 release |
| 局部标量(int、结构体等) | 值拷贝 | 不涉及引用计数 |
| __block 修饰的变量 | 生成结构体,block 与外部共享 | 若 __block 变量指向 OC 对象,需注意 MRC/ARC 下 retain 行为;__block 可改写 |
| __weak 修饰的对象 | 弱引用 | Block 不持有该对象,不增加引用计数,可避免循环引用 |
3.2 对象捕获与循环引用
- Block 若强引用了某个对象 A(如直接使用
self),而 A 又强引用了该 block(如 block 被 A 的 strong/copy 属性持有),则形成 self → block → self 的循环,两者都不会释放。
- 解决:在 block 外使用
__weak typeof(self) wself = self,block 内使用 wself,这样 block 对 self 是弱引用;若在 block 执行过程中担心 self 被释放,可在 block 内再用 __strong typeof(wself) sself = wself 强引用一次(仅限 block 执行期),避免执行到一半 self 被置 nil。详见 06-weak与循环引用。
3.3 __block 与内存(简述)
- __block 使局部变量在 block 内可被修改,编译器会生成一个包装结构,block 捕获的是该结构;若 __block 变量指向 OC 对象,在 ARC 下通常不会造成 block 对对象的强引用(对象存在 __block 结构里),但若在 block 内给该变量赋新值,会涉及旧值 release、新值 retain。MRC 下 __block 不会自动 retain 对象,需自行管理。
- 历史上用 __block 打破循环(__block self + block 内置 nil)的写法在 ARC 下不推荐,应使用 __weak 打破循环。
四、Block 作为属性、参数与返回值
4.1 属性声明
| 属性修饰 | 说明 |
|---|
| copy | 设值时对 block 执行 copy;MRC 时代推荐,ARC 下 strong 与 copy 对 block 效果类似(都会 copy 到堆),习惯上仍常用 copy 表达「这是 block」的语义。 |
| strong | ARC 下与 copy 类似,赋值时也会把栈 block copy 到堆并强引用。 |
- Block 属性应避免用 assign(栈 block 离开作用域后失效,会野指针)。
4.2 作为参数与返回值
- 作为参数:若 API 会保存 block(如延迟执行、存入数组),API 内部应对传入的 block 做 copy(或由 ARC 在传入时保证是堆 block);调用方传栈 block 时,由被调用方 copy 到堆是常见约定。
- 作为返回值:返回 block 时,若希望调用方在函数返回后仍能使用,应返回堆上的 block(MRC 下 return 前对 block 做 copy/autorelease;ARC 下编译器会根据返回类型自动处理)。
五、ARC 与 MRC 下 Block 内存小结
| 场景 | MRC | ARC |
|---|
| 栈 block 需跨作用域使用 | 必须对 block 执行 copy,用完后 release | 编译器在赋值给 strong/copy、传参等场景自动 copy |
| Block 属性 | 用 copy,setter 里对 block copy、对旧 block release | copy 或 strong 均可,都会导致 copy 到堆并强引用 |
| Block 内引用 self | 避免 self→block→self:用 __weak 或 __block+置 nil | 用 __weak self,必要时 block 内 __strong 一次 |
| Block 捕获的 OC 对象 | copy 到堆时 block 会 retain 捕获的对象;block release 时 release 这些对象 | 同左,由编译器插入 |
六、流程图:Block 从创建到释放(概念)
flowchart LR
A[定义 Block] --> B{是否捕获自动变量?}
B -->|否| C[__NSGlobalBlock__ 全局]
B -->|是| D[__NSStackBlock__ 栈]
D --> E[赋值给 strong/copy 或 传参]
E --> F[copy 到堆 __NSMallocBlock__]
F --> G[Block 被 release]
G --> H[对捕获对象 release]
七、Swift 闭包与内存
- Swift 闭包 与 OC Block 语义对应:闭包会捕获外部变量,默认对类对象是强引用。
- 循环引用:若对象强引用闭包,闭包内又使用了
self(或捕获了 self),则形成循环;解决方式为在闭包捕获列表中写 [weak self] 或 [unowned self](后者在 self 一定不会先于闭包释放时使用,否则会野指针)。
- @escaping:标记闭包会「逃逸」出当前函数(如异步回调),编译器会按需将闭包拷贝到堆上,与 OC 中「block 被 copy 到堆」对应。
八、思维导图:Block 内存管理知识结构
mindmap
root((Block 内存管理))
三种类型
全局 Block 无捕获
栈 Block 捕获未 copy
堆 Block copy 后
捕获与引用
对象默认强引用
__weak 破循环
__block 可改写
属性与生命周期
copy/strong 属性
MRC 需手写 copy
ARC 自动 copy
循环引用
self → block → self
weak self strong self
九、参考文献