在 Objective-C 中,Block 本质上是一个封装了函数及其执行上下文(变量捕获)的 OC 对象。根据其内存存储位置的不同,Block 分为三种主要的存储类型。
你可以通过查看 Block 对象的 isa 指针指向来判定它的类型。
1. _NSConcreteStackBlock(栈 Block)
这是 Block 的“原始形态”。在开启 ARC 之后,虽然我们很难直接观察到它(因为它经常被自动拷贝到堆上),但在底层逻辑中它依然存在。
- 存储位置:函数执行的栈内存中。
- 特征:只在定义它的函数作用域内有效。一旦函数返回,栈帧销毁,该 Block 也会被立即回收。
- 触发条件:Block 访问了外部变量(捕获了变量),但还没有被执行
copy操作。
风险:在 MRC 时代,直接返回一个栈 Block 是非常危险的,因为函数返回后,外部访问该 Block 会直接导致野指针崩溃。
2. _NSConcreteGlobalBlock(全局 Block)
这是最省心的一种类型,它的行为类似于普通的全局函数。
-
存储位置:程序的数据区(Data Segment / .data) 。
-
特征:生命周期贯穿整个程序运行期间,不会被销毁。
-
触发条件:
- Block 内部没有访问任何外部变量(包括局部变量、
self等)。 - 或者是定义在函数体之外的全局 Block。
- Block 内部没有访问任何外部变量(包括局部变量、
3. _NSConcreteMallocBlock(堆 Block)
这是我们在开发中最常打交道的类型。在 ARC 下,编译器会为了保证安全,频繁地将栈 Block “搬”到堆上。
-
存储位置:堆内存(Heap) 。
-
特征:引用计数管理。只有当它的引用计数降为 0 时,才会被销毁。它可以在不同的作用域之间传递。
-
触发条件:对一个栈 Block 执行了
copy操作。-
在 ARC 下,以下情况会自动执行 copy:
- 调用
strong属性指向 Block。 - 将 Block 作为函数返回值返回。
- 将 Block 传给 Cocoa 框架中带有
usingBlock的方法(如 GCD API 或集合遍历)。
- 调用
-
三种类型对比汇总
| 类型 | 存储位置 | 变量捕获情况 | 生命周期 |
|---|---|---|---|
| 全局 Block | 数据区 | 不捕获任何外部变量 | 永久存在 |
| 栈 Block | 栈区 | 捕获了变量,未被拷贝 | 随函数作用域结束 |
| 堆 Block | 堆区 | 捕获了变量,且执行了 copy | 随引用计数清零 |
为什么我们要关心这个?
了解这个分类能帮你解开很多 “为什么” :
- 为什么 Block 属性用
copy? 为了确保如果是栈 Block,能被正确搬到堆上,防止作用域结束后失效。 - 为什么 Block 能捕获
__block变量? 当 Block 从栈拷贝到堆时,它会顺带着把引用的__block变量也拷贝到堆上,从而实现跨作用域的变量修改。