全面剖析 Block 的本质、底层结构、内存管理、变量捕获、循环引用、线程安全、调试技巧与最佳实践。 力求深入而易懂------用类比代替术语堆砌,用图表代替大段代码。
一、Block 的本质
Block 是 C 语言层面的匿名函数 + 自动捕获上下文变量的能力 的组合体。
它不是 Objective-C 独有的特性,而是 Apple 对 C 语言的扩展(Clang 编译器实现),所以在 C、C++、Objective-C、Objective-C++ 中都可以使用。
一句话概括 Block 的本质:
Block 是一个封装了函数指针和捕获变量的 Objective-C 对象。
这意味着 Block 同时具备两重身份:
- 作为函数:可以被调用、传参、返回
- 作为对象 :有
isa指针,可以被copy/release,参与 ARC 内存管理
类比理解: 普通函数像一台固定在车间里的机器------你去找它,它帮你加工。Block 像一台可以搬走的便携机器------你能带着它走,而且它随身携带了自己需要的原材料(捕获的变量)。
1.1 从编译器视角看 Block 的诞生
Clang 编译 Block 时经历的变换过程:
scss
源代码层 编译器中间表示层 机器码层
^{ NSLog(@"Hi"); }
│
▼
语法解析为 BlockExpr
│
▼
分析捕获变量列表
(遍历 Block 体内所有引用的外部变量)
│
▼
生成 Block_layout 结构体定义
(根据捕获变量数量和类型动态确定结构体大小)
│
▼
将 Block 体内的代码提取为一个独立的 C 函数
(函数名通常为 __文件名_block_func_序号)
(第一个参数为 Block 结构体指针)
│
▼
在原位置生成结构体初始化代码
(填充 isa、invoke、descriptor、捕获变量)
│
▼
ARM64 机器码
关键洞察: Block 的"闭包"能力不是运行时魔法,而是编译期的代码变换------编译器帮你把自由变量"打包"进了一个结构体。这就像你要出差,把办公桌上需要的文件全装进行李箱带走,到了酒店打开就能继续工作。
1.2 Block 与其他语言闭包的本质差异
scss
┌──────────────┬────────────────────────────────────────────┐
│ 语言 │ 闭包实现方式 │
├──────────────┼────────────────────────────────────────────┤
│ JavaScript │ 通过作用域链引用外部变量(共享同一个变量) │
│ │ 闭包和外部代码修改的是同一个变量 │
│ │ 不需要特殊关键字 │
├──────────────┼────────────────────────────────────────────┤
│ Swift │ 默认捕获引用(和 JS 类似,共享变量) │
│ │ [value] 显式值捕获 │
│ │ 闭包是引用类型 │
├──────────────┼────────────────────────────────────────────┤
│ OC Block │ 默认值捕获(拷贝一份副本) │
│ │ 需要 __block 才能共享变量 │
│ │ Block 有 Stack/Malloc/Global 三种存储位置 │
│ │ 需要显式/隐式 copy 才能延长生命周期 │
├──────────────┼────────────────────────────────────────────┤
│ C++ Lambda │ [=] 值捕获,[&] 引用捕获 │
│ │ 编译为匿名类的 operator() │
│ │ 和 OC Block 最相似 │
└──────────────┴────────────────────────────────────────────┘
OC Block 的独特之处:
1. 默认值捕获 → 安全但反直觉(修改需要 __block)
2. 有栈→堆迁移的概念 → 其他语言的闭包都直接在堆上
3. 是 OC 对象 → 参与引用计数,有循环引用问题
1.3 为什么 Apple 选择了默认"值捕获"
这个设计决策背后有深层考虑:
javascript
JavaScript 的"引用捕获"经常制造 Bug:
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 0);
}
// 输出 3, 3, 3(而非期望的 0, 1, 2)
// 因为闭包共享了变量 i,循环结束时 i = 3
OC 的"值捕获"避免了这类问题:
for (int i = 0; i < 3; i++) {
dispatch_async(queue, ^{ NSLog(@"%d", i); });
}
// 输出 0, 1, 2 ✓
// 每个 Block 在创建时拍了一张 i 的快照
Apple 的设计哲学:
"大多数场景下,Block 只需要读取变量的值,不需要修改"
"让安全的事情成为默认,不安全的事情需要显式声明(__block)"
这是典型的 Pit of Success 设计理念 ------
让开发者默认就掉进成功的坑里,想犯错反而需要额外的努力。
二、Block 的底层结构
编译器会将每个 Block 转换为一个结构体 + 一个函数。
scss
Block 变量(指针)
│
▼
┌──────────────────────────────────────┐
│ Block_layout 结构体 │
├──────────────────────────────────────┤
│ isa 指针 → 指向 Block 的类 │
│ flags → 标志位 │
│ reserved → 保留字段 │
│ invoke → 函数指针 (核心) │
│ descriptor → 描述信息指针 │
│ ─────────────────────────────────── │
│ captured_var_1 → 捕获的变量 1 │
│ captured_var_2 → 捕获的变量 2 │
│ ... │
└──────────────────────────────────────┘
类比: 把 Block 想象成一个快递包裹:
isa是包裹类型标签(普通件/到付件/国际件)flags是物流状态码invoke是"使用说明书"(告诉你怎么打开和使用内容物)descriptor是"装箱清单"(描述包裹尺寸和特殊处理要求)- 捕获的变量就是包裹里的内容物
2.1 flags 标志位详解
flags 不是一个简单的整数,它是一个位域(bitfield),每一位都有特定含义:
arduino
flags 位域布局 (32 bit):
31 30 29 28 27 26 25 24 16 15 1 0
┌───┬───┬───┬───┬───┬───┬───┬──── ─ ────┬───┬──── ─ ───┬───┐
│ │ │ │ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴──── ─ ────┴───┴──── ─ ───┴───┘
│ │ │ │ │ │
│ │ │ │ │ └── bit 0: BLOCK_DEALLOCATING
│ │ │ │ │ 正在被释放
│ │ │ │ │
│ │ │ │ └── bit 1~15: 引用计数(存储在这里!)
│ │ │ │ 堆上 Block 的 retainCount
│ │ │ │
│ │ │ └── bit 24: BLOCK_NEEDS_FREE
│ │ │ 表示是堆上的 Block,需要 free 释放
│ │ │
│ │ └── bit 25: BLOCK_HAS_COPY_DISPOSE
│ │ 表示有 copy_helper 和 dispose_helper
│ │ (即捕获了对象或 __block 变量)
│ │
│ └── bit 26: BLOCK_HAS_CTOR
│ 捕获的变量有 C++ 构造函数
│
└── bit 30: BLOCK_HAS_SIGNATURE
表示有方法签名(可通过 NSMethodSignature 获取参数/返回值类型)
隐藏知识: Block 的引用计数不像普通 OC 对象存在 SideTable 中,而是直接编码在 flags 的 bit 1~15 中。这是一个性能优化------避免了每次 retain/release 都要查 SideTable 的哈希表。这意味着 Block 最大引用计数为 2^15 - 1 = 32767,不过实际场景中绰绰有余。
2.2 descriptor 的多态结构
descriptor 不是固定结构,它根据 Block 捕获的内容有不同的版本:
c
版本 1(不捕获对象/不捕获 __block 变量):
┌─────────────────────────────┐
│ unsigned long reserved │ → 0
│ unsigned long size │ → Block 结构体总字节数
└─────────────────────────────┘
版本 2(捕获了对象或 __block 变量,BLOCK_HAS_COPY_DISPOSE = 1):
┌─────────────────────────────┐
│ unsigned long reserved │
│ unsigned long size │
│ void (*copy_helper)() │ → 新增:拷贝时调用
│ void (*dispose_helper)() │ → 新增:销毁时调用
└─────────────────────────────┘
版本 3(有方法签名,BLOCK_HAS_SIGNATURE = 1):
┌─────────────────────────────┐
│ unsigned long reserved │
│ unsigned long size │
│ void (*copy_helper)() │ → 可能有
│ void (*dispose_helper)() │ → 可能有
│ const char *signature │ → 新增:Block 的类型编码字符串
└─────────────────────────────┘
例如 signature = "v8@?0" 表示 void(^)(void)
这种多态设计的好处:
- 不捕获对象的 Block,descriptor 更小,节省内存
- 编译器根据情况选择最紧凑的版本
类比:就像飞机上的行李标签------
国内经济舱行李只需要简单标签(版本 1)
含易碎品的行李需要额外的特殊处理标签(版本 2)
国际航班行李还需要海关申报信息(版本 3)
2.3 invoke 函数的隐含参数
scss
当你写这样的代码:
int x = 10;
void(^block)(int) = ^(int y) { printf("%d", x + y); };
block(20);
编译器生成的 invoke 函数签名是:
static void __main_block_func_0(
struct __main_block_impl_0 *__cself, // ← 隐含的第一个参数!
int y // ← 你写的参数
)
调用过程:
block(20)
→ block->invoke(block, 20)
↑ Block 把自己作为第一个参数传入
这样 invoke 函数就能访问 Block 结构体中捕获的变量
这和 OC 方法调用极其类似:
[obj doSomething] → objc_msgSend(obj, @selector(doSomething))
block(20) → block->invoke(block, 20)
2.4 Block 的类型编码(Type Encoding)
Block 在 runtime 中也有类型签名,这让它可以与 NSInvocation 配合使用:
objectivec
Block 签名编码规则:
void(^)(void) → "v8@?0"
void(^)(int) → "v12@?0i8"
NSString *(^)(int, BOOL) → "@20@?0i8B12"
编码字符含义:
v = void
@ = 对象指针
@? = Block 类型(@ 表示对象,? 表示 Block)
i = int
B = BOOL
数字 = 参数在栈帧中的偏移量
获取 Block 签名的方式:
从 descriptor 的 signature 字段读取
可用 NSMethodSignature 解析为可读格式
这个签名使得 Block 可以被 NSInvocation 动态调用,
也使得 libffi 能够在运行时对 Block 做各种 hook 和转发操作。
三、Block 的三种类型
Block 根据存储位置分为三种类型,这是理解 Block 内存管理的关键:
scss
┌─────────────────────────────────────────────────────────────┐
│ 内存布局 │
│ │
│ 高地址 ┌──────────────┐ │
│ │ 栈 Stack │ ← __NSStackBlock__ │
│ │ (向下增长) │ 栈上的 Block,离开作用域即销毁 │
│ ├──────────────┤ │
│ │ │ │
│ │ 堆 Heap │ ← __NSMallocBlock__ │
│ │ │ 堆上的 Block,引用计数管理 │
│ ├──────────────┤ │
│ │ 全局/静态区 │ ← __NSGlobalBlock__ │
│ │ Data Segment│ 全局 Block,程序结束才销毁 │
│ ├──────────────┤ │
│ 低地址 │ 代码区 Text │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.1 三种类型的判定规则
| 类型 | 条件 | 生命周期 | isa 指向 |
|---|---|---|---|
__NSGlobalBlock__ |
不捕获任何外部局部变量 | 与程序同生共死 | _NSConcreteGlobalBlock |
__NSStackBlock__ |
捕获了外部局部变量(MRC 或未被强引用) | 与所在栈帧同生共死 | _NSConcreteStackBlock |
__NSMallocBlock__ |
Stack Block 经 copy 后 |
引用计数为 0 时销毁 | _NSConcreteMallocBlock |
类比理解:
- 全局 Block → 写在教科书上的公式------永远在那里,谁都能用
- 栈 Block → 写在白板上的草稿------会议结束就擦掉
- 堆 Block → 拍了照片保存在手机里的公式------照片在就在,删了就没了
3.2 三种类型的 retain/copy/release 行为差异
objectivec
retain copy release
────── ──── ───────
GlobalBlock 什么都不做 返回自身 什么都不做
(它是不死的) (不需要 copy) (不能被释放)
StackBlock 什么都不做 拷贝到堆上 什么都不做
(栈管理) 返回堆上新副本 (栈自动管理)
★这是唯一有效操作
MallocBlock 引用计数 +1 引用计数 +1 引用计数 -1
(不会重新拷贝) (归零则释放)
关键洞察:
- 对 StackBlock 做 retain 是无效的(不增加引用计数)
- 必须 copy 才能将其"救"到堆上
- 这就是为什么 Block 属性要用 copy 而不是 strong(MRC 时代的历史原因)
- ARC 下 strong 和 copy 对 Block 效果相同(编译器自动插入 copy)
面试陷阱: "对一个已经在堆上的 Block 再做 copy 会发生什么?"------答案是只增加引用计数(+1),不会创建新副本 。这和
NSMutableArray copy会创建新对象是不同的行为。
3.3 ARC 下的自动 copy ------ 编译器到底做了什么
在 ARC 环境下,以下场景编译器会自动将栈 Block copy 到堆上:
scss
自动 copy 触发场景:
├── ① Block 作为函数/方法返回值
│ 编译器在 return 语句处插入 objc_retainBlock() → 内部调用 _Block_copy()
│
├── ② Block 赋值给 __strong 修饰的变量
│ void(^block)(void) = ^{ ... };
│ 编译器在赋值处插入 objc_retainBlock()
│
├── ③ Block 作为 GCD API 的参数
│ dispatch_async 内部实现中调用 _Block_copy()
│
├── ④ Block 作为 Cocoa 框架中含 usingBlock 的方法参数
│ 框架内部负责 copy
│
└── ⑤ Block 被传入方法的参数(编译器启发式判断)
如果方法签名暗示会保存 Block(如 completion handler),编译器插入 copy
编译器不会自动 copy 的场景(ARC 下罕见但存在):
├── Block 作为函数参数传递时,如果调用者没有保存它
│ (此时 Block 可能在栈上,但因为是同步使用所以安全)
└── 使用 __unsafe_unretained 修饰的变量接收 Block
3.4 为什么 Block 要有栈这个中间状态?
scss
设计哲学:性能与安全的权衡
如果 Block 一律在堆上创建(像 Swift 闭包那样):
✅ 简单,不需要考虑栈→堆迁移
❌ 每次创建 Block 都要 malloc,频繁的堆分配 + GC 压力
OC Block 的策略:
✅ 大量临时 Block(如 for 循环中的 Block)直接在栈上创建和销毁
无 malloc 开销、无引用计数开销
✅ 只有需要"逃逸"的 Block 才 copy 到堆上
⚠️ 代价是引入了栈→堆迁移的复杂性
类比理解:
栈 Block → 临时工,干完活就走,不占编制
堆 Block → 正式员工,办了入职手续(malloc),有档案(引用计数)
全局 Block → 终身教授,和学校同在
在 ARC 时代,编译器自动处理迁移,开发者几乎无感。
但理解这个机制对排查内存问题至关重要。
3.5 逃逸(Escape)与非逃逸(Non-Escape)
这个概念虽然在 Swift 中用 @escaping 关键字显式化了,但在 OC Block 中一直存在,只是没有语法层面的区分:
less
什么是逃逸?
Block 的使用范围"逃"出了它被创建的那个函数作用域。
非逃逸 Block(不需要 copy 到堆上):
- 在当前函数内同步调用然后丢弃
- 例如:enumerateObjectsUsingBlock、sortUsingComparator
- 函数结束时 Block 还在栈上,随栈帧销毁即可
逃逸 Block(必须 copy 到堆上):
- 被保存到属性/实例变量中
- 被异步 dispatch 执行
- 作为返回值传出当前函数
- 被传入会保存它的 API(如 NSNotificationCenter 的 block observer)
逃逸 Block 的特征:
┌──────────────────────────────────────────────────────┐
│ Block 在创建者的作用域结束之后仍然可能被调用 → 逃逸 │
│ Block 只在创建者的作用域内被调用 → 非逃逸 │
└──────────────────────────────────────────────────────┘
Swift 把这个概念升级为编译器强制检查:
- func doWork(completion: @escaping () -> Void) // 编译器知道会逃逸
- func doWork(block: () -> Void) // 默认非逃逸,性能更好
OC 中虽然没有编译器检查,但 ARC 的自动 copy 机制帮你兜了底:
该 copy 的时候自动 copy,不需要显式操心。
四、变量捕获机制
这是 Block 最核心、最容易出问题的部分。
4.1 捕获规则总览
scss
┌──────────────────┬────────────────────┬──────────────────────┐
│ 变量类型 │ 捕获方式 │ Block 内能否修改 │
├──────────────────┼────────────────────┼──────────────────────┤
│ 局部基本类型变量 │ 值拷贝 (copy) │ 不能(编译报错) │
│ 局部对象类型变量 │ 指针值拷贝 (copy) │ 不能改指针指向 │
│ │ │ 能改指向对象的属性 │
│ __block 局部变量 │ 封装为堆上结构体引用 │ 能 │
│ 静态局部变量 │ 指针引用 (不拷贝) │ 能 │
│ 全局变量 │ 不捕获 (直接访问) │ 能 │
│ self │ 强引用捕获 │ 能访问属性/方法 │
└──────────────────┴────────────────────┴──────────────────────┘
4.2 局部变量的值捕获 ------ 为什么是"快照"
ini
捕获时刻:Block 定义时(不是调用时)
int a = 10;
void(^block)(void) = ^{
// 这里的 a 是定义时拷贝进来的值 = 10
};
a = 20;
block(); // 输出 10,不是 20
原理: 编译器在 Block 结构体中新增了一个 int a 成员变量,在 Block 创建的那一刻,将外部 a 的值拷贝 进去。之后外部 a 的变化与 Block 内部无关。
ini
Block 结构体 外部栈帧
┌──────────────┐ ┌──────────────┐
│ isa │ │ │
│ invoke │ │ a = 20 │ ← 外部已改为 20
│ ... │ │ │
│ a = 10 │ ← 独立副本 │ │
└──────────────┘ └──────────────┘
↑ Block 内部读到的始终是 10
为什么编译器禁止修改? 不是技术上不能改(结构体成员当然可以改),而是改了会造成语义困惑 ------开发者期望改的是外部变量 a,但实际改的是内部副本,外部完全无感。编译器选择了"报错"而非"允许但行为诡异"。
类比: 值捕获就像你拍了一张照片。照片定格了那一瞬间的画面。之后现实场景怎么变化,照片里的内容不会变。而且你在照片上涂改也改不了现实。
4.3 对象类型的值捕获 ------ 指针拷贝的微妙之处
ini
对象类型的捕获容易让人困惑,因为需要区分"指针"和"指针所指的对象":
NSMutableArray *arr = [NSMutableArray new];
void(^block)(void) = ^{
[arr addObject:@"hello"]; // ✅ 合法!
arr = [NSMutableArray new]; // ❌ 编译报错!
};
为什么前者合法,后者报错?
Block 拷贝的是"指针的值"(即对象的地址),不是对象本身。
┌───── Block 结构体 ─────┐ ┌─── NSMutableArray ───┐
│ │ │ │
│ arr = 0x1234 (拷贝) ──┼────→ │ contents: [...] │
│ │ │ │
└────────────────────────┘ └──────────────────────┘
↑
┌───── 外部栈帧 ─────────┐ │
│ │ │
│ arr = 0x1234 (原件) ──┼───────────────┘
│ │
└────────────────────────┘
两个 arr 指针虽然是独立副本,但都指向同一个 NSMutableArray 对象。
所以:
- [arr addObject:] → 通过指针操作对象 → 合法(没改指针本身)
- arr = xxx → 修改指针本身 → 报错(和修改 int a 是一样的道理)
4.4 静态局部变量与全局变量 ------ 为什么不需要捕获
scss
为什么静态变量和全局变量不需要"捕获"?
全局变量:
存储在数据段(Data Segment),地址在编译期确定
程序任何地方都可以通过固定地址直接访问
→ Block 内直接用地址访问,不需要拷贝到结构体
静态局部变量:
虽然作用域是局部的,但存储在数据段
地址在编译期确定,生命周期是全程序
→ Block 捕获的是变量的指针(地址),不是值
→ 通过指针间接访问,所以能读也能改
Block 结构体中存储的是:int *countPtr = &count;
访问时:*(countPtr) = *(countPtr) + 1;
对比:
局部变量 → 值拷贝(因为栈帧会销毁,地址会失效)
静态变量 → 指针拷贝(地址永远有效)
全局变量 → 不捕获(直接用符号地址)
类比:
局部变量 → 别人白板上的草稿,你必须抄一份带走(值拷贝)
静态变量 → 图书馆书架上的书,你只需要记住书架号(指针拷贝)
全局变量 → 墙上的公告,不需要抄,大家都能看到(不捕获)
4.5 __block 修饰符的深层原理
__block 不是简单的"允许修改",它触发了一个复杂的底层变换:
ini
原始代码: 编译器转换后:
__block int a = 10; → struct __Block_byref_a {
void *__isa;
__Block_byref_a *__forwarding; // 关键!
int __flags;
int __size;
int a; // 真正的值
};
__Block_byref_a a = {
.isa = 0,
.__forwarding = &a, // 指向自己
.a = 10
};
理解 __block 的类比: 普通变量捕获就像你把书抄了一页带走------你在副本上改东西,原书不变。而 __block 就像把整本书放进一个共享文件柜(堆上的 __Block_byref 结构体),然后给所有需要的人配一把钥匙(指针)------大家打开柜子看到的、改的都是同一本书。
__forwarding 指针的精妙设计
css
╔══════════════════════════════════════════════════════════════════╗
║ __forwarding 存在的根本原因: ║
║ 解决"同一个 __block 变量可能同时被栈上代码和堆上 Block 访问"的问题 ║
╚══════════════════════════════════════════════════════════════════╝
阶段一:Block 还在栈上时
Stack 栈帧
┌─────────────────────┐
│ __Block_byref_a │
│ ├── __forwarding ───┼──→ 指向自己(栈上地址)
│ └── a = 10 │
└─────────────────────┘
此时所有访问 a 的代码都通过:byref->__forwarding->a
由于 __forwarding 指向自己,等价于 byref->a
看起来多此一举?往下看------
阶段二:Block 被 copy 到堆上
Stack Heap
┌─────────────────────┐ ┌─────────────────────┐
│ __Block_byref_a │ │ __Block_byref_a │
│ ├── __forwarding ───┼──┬──┼── __forwarding ───┼──→ 指向自己(堆上)
│ └── a = 10 │ │ │ └── a = 10 │
└─────────────────────┘ │ └─────────────────────┘
│ ↑
└─────────┘
栈上的 __forwarding 被修改!
现在指向堆上副本
关键效果:
- 栈上代码访问 a → byref_stack->__forwarding->a → 堆上的 a ✓
- Block 内访问 a → byref_heap->__forwarding->a → 堆上的 a ✓
- 两边修改的是同一个 a!
如果没有 __forwarding:
- 栈上代码修改 byref_stack->a = 20,改的是栈上的副本
- Block 内读取 byref_heap->a,读的是堆上的副本
- 两边不一致!Bug!
类比: __forwarding 就像一个邮件转发地址。一开始你住在家里(栈上),所有信都寄到家里地址。后来你搬到公司(堆上),你在家里设置了邮件转发------所有寄到家里的信都会自动转到公司。这样无论别人往哪个地址寄信,最终都送到你手上(堆上)。
__block 变量被多个 Block 捕获时
objectivec
__block int a = 10;
void(^block1)(void) = ^{ a = 20; };
void(^block2)(void) = ^{ a = 30; };
底层发生了什么?
block1 copy 到堆时:
→ __Block_byref_a 从栈 copy 到堆(第一次)
→ block1 结构体中保存指向堆上 byref 的指针
block2 copy 到堆时:
→ 发现 __Block_byref_a 已经在堆上了(通过 __forwarding 判断)
→ 不再重复 copy,直接引用计数 +1
→ block2 结构体中保存同一个堆上 byref 的指针
结果:
block1 和 block2 共享同一个堆上的 __Block_byref_a
修改的是同一个 a
__Block_byref_a 的引用计数 = 2(被两个 Block 持有)
__block 与对象类型的特殊交互(MRC vs ARC)
objectivec
__block 在 MRC 和 ARC 下对对象类型变量的行为完全不同!
MRC 下:
__block id obj = [[NSObject alloc] init];
→ __Block_byref 结构体中的 obj 不会被 retain
→ 可以用来避免循环引用(因为不强引用)
→ 这是 MRC 时代避免循环引用的手段之一
ARC 下:
__block id obj = [[NSObject alloc] init];
→ __Block_byref 结构体中的 obj 会被 strong 引用
→ 不能用来避免循环引用!
→ 这是很多从 MRC 迁移到 ARC 的项目踩过的坑
ARC 下避免循环引用应该用 __weak,不是 __block
总结:
MRC: __block 不 retain 对象 → 可用于打破循环引用
ARC: __block 会 retain 对象 → 不能打破循环引用,需要 __weak
4.6 对象类型变量的捕获与内存管理
objectivec
捕获对象时的引用关系:
NSObject *obj = [[NSObject alloc] init];
void(^block)(void) = ^{
NSLog(@"%@", obj);
};
┌──────────────────┐ ┌───────────────┐
│ Block (堆上) │ │ NSObject 实例 │
│ ├── isa │ │ │
│ ├── invoke │ │ retainCount │
│ └── obj (strong) ──┼────→ │ (被 Block +1) │
└──────────────────┘ └───────────────┘
Block copy 到堆时,会调用 descriptor 中的 copy_helper,
对捕获的对象执行 _Block_object_assign:
- 强引用 (默认) → 等同于 retain
- __weak 修饰 → 弱引用,不增加引用计数
- __unsafe_unretained → 不增加引用计数,不置 nil(危险)
_Block_object_assign 的内部逻辑
typescript
void _Block_object_assign(void *destAddr, const void *object, int flags) {
flags 决定行为:
BLOCK_FIELD_IS_OBJECT (3): // 捕获的是 OC 对象
→ _Block_retain_object(object)
→ 等价于 [object retain] 或 objc_storeStrong
→ 如果是 __weak,走 objc_initWeak 路径
BLOCK_FIELD_IS_BLOCK (7): // 捕获的是另一个 Block
→ _Block_copy(object)
→ 被捕获的 Block 也会被 copy 到堆上(递归 copy)
BLOCK_FIELD_IS_BYREF (8): // 捕获的是 __block 变量
→ _Block_byref_copy(object)
→ 将 __block 结构体 copy 到堆上
→ 修改 __forwarding 指针
BLOCK_FIELD_IS_WEAK (16): // __weak 修饰的对象
→ objc_initWeak(destAddr, object)
→ 注册到 SideTable 的弱引用表中
}
4.7 self 的捕获 ------ 隐式 vs 显式
scss
═══════════════════════════════════════════════════════════════
self 捕获是循环引用的最大根源,必须深入理解
═══════════════════════════════════════════════════════════════
显式捕获(容易意识到):
^{ [self doSomething]; } // 明确写了 self
^{ self.name = @"xxx"; } // 明确写了 self
隐式捕获(容易忽略!):
^{ _name = @"xxx"; } // 直接访问 ivar,编译器转为 self->_name
// 同样强引用捕获了 self!
^{ [self->_delegate call]; } // 同理
^{ _block(); } // 如果 _block 是实例变量
// 也隐式捕获了 self
更隐蔽的情况:
^{ doSomething(); } // 如果 doSomething 是当前类的方法
// 编译器转为 [self doSomething]
// 隐式捕获 self
^{ NSLog(@"%@", _array[0]); } // _array 是 ivar → 捕获了 self
┌──────────────────────────────────────────────────────┐
│ 规则:Block 内只要访问了实例变量或调用了实例方法, │
│ 就一定捕获了 self,无论有没有写 "self." 前缀 │
└──────────────────────────────────────────────────────┘
4.8 捕获变量的内存对齐
java
Block 结构体中捕获变量的排列遵循 C 语言的内存对齐规则:
假设捕获了以下变量:
char c; // 1 byte
int i; // 4 bytes
double d; // 8 bytes
在 Block 结构体中的排列:
偏移量 0~7: Block_layout 固有字段(isa, flags 等)
...
偏移量 N: char c (1 byte)
偏移量 N+1~N+3: padding (3 bytes 填充,对齐到 4)
偏移量 N+4: int i (4 bytes)
偏移量 N+8: double d (8 bytes)
编译器的优化:
有时编译器会重新排列捕获变量的顺序
把大类型放前面,小类型放后面
以减少 padding,使 Block 结构体更紧凑
这对日常开发影响不大,但在分析 crash log 中 Block 结构体
的内存布局时,理解对齐规则有助于定位问题。
五、循环引用的本质与解法
5.1 循环引用的形成
objectivec
经典循环引用:
self → 强引用 → block → 强引用 → self
┌──────────┐ strong ┌──────────┐ strong ┌──────────┐
│ self │─────────→│ Block │─────────→│ self │
│ (对象) │ │ (堆上) │ │ (同一个) │
└──────────┘ └──────────┘ └──────────┘
↑ │
└──────────────────────────────────────────┘
引用计数永远不归零,两者都无法释放 → 内存泄漏
类比: 循环引用就像两个人互相握手不肯先松开------只要对方不松手我也不松手,结果两个人永远僵持在那里。__weak 就是让其中一个人用力气很小的方式搭在对方手上(不算真正的"握"),一旦对方松开,自己的手自动滑落。
5.2 复杂循环引用链(实际项目中更常见)
swift
不是所有循环引用都是直接的 self → block → self
间接循环引用(三角环):
self → viewModel → completionBlock → self
ViewController 持有 ViewModel
ViewModel 持有 completionBlock
completionBlock 捕获了 ViewController(self)
→ 三者形成环,都无法释放
更隐蔽的多层环:
self → manager → handler → service → callback → self
┌────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
│ self │──→│ manager │──→│ handler │──→│ callback │
└────────┘ └─────────┘ └─────────┘ └──────────┘
↑ │
└────────────────────────────────────────────┘
NSTimer 的经典循环引用:
self → timer (strong)
timer → target: self (strong,NSTimer 强引用 target)
timer → block (如果用 block API)
NSTimer 的特殊性:
- RunLoop 强引用 timer
- timer 强引用 target (self)
- 即使 self 不强引用 timer,RunLoop → timer → self 也会导致 self 不释放
- 必须手动 invalidate timer 才能打破
NSTimer 循环引用的根本原因:
┌──────────────────────────────────────────────────────────┐
│ NSTimer 的设计缺陷:它强引用 target 直到 invalidate。 │
│ 这打破了常规的"弱引用 delegate"范式。 │
│ 解决方案:使用 iOS 10+ 的 block-based API + weakSelf, │
│ 或者使用 NSProxy 中间人模式。 │
└──────────────────────────────────────────────────────────┘
5.3 解决方案的原理对比
swift
方案一:__weak(推荐)
self → strong → Block → weak → self
↑
弱引用不增加引用计数
self 释放后自动置 nil
特点:
✅ 安全,self 释放后 weakSelf 自动为 nil
✅ Block 调用时需要判断 weakSelf 是否为 nil
⚠️ Block 执行过程中 self 可能随时被释放(多线程场景)
─────────────────────────────────────────
方案二:__weak + __strong(Weak-Strong Dance)
Block 外部:__weak typeof(self) weakSelf = self;
Block 内部:__strong typeof(weakSelf) strongSelf = weakSelf;
执行流程:
① Block 被调用
② strongSelf = weakSelf(如果 self 已释放,strongSelf = nil,提前 return)
③ strongSelf 临时持有 self,保证 Block 执行期间 self 不被释放
④ Block 执行完毕,strongSelf 出栈,临时强引用消失
特点:
✅ 保证 Block 执行期间 self 存活
✅ Block 执行完后不阻止 self 释放
✅ 最佳实践
─────────────────────────────────────────
方案三:__block + 手动置 nil(MRC 遗留思路)
__block typeof(self) blockSelf = self;
self.block = ^{
[blockSelf doSomething];
blockSelf = nil; // 手动打破循环
};
self → Block → blockSelf → self(执行后 blockSelf = nil 打破)
特点:
⚠️ Block 必须被执行才能打破循环
⚠️ 如果 Block 永远不被调用 → 内存泄漏
❌ 不推荐
5.4 Weak-Strong Dance 的深层理解
ini
为什么单纯 __weak 在某些场景下不够?
场景:Block 执行到一半时 self 被释放
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doStep1]; // ① weakSelf 非 nil,执行成功
// ──── 此时另一个线程释放了 self ────
[weakSelf doStep2]; // ② weakSelf 变成 nil!不执行了
weakSelf.name = @"xx"; // ③ 也不执行
// 步骤不完整,数据可能处于不一致状态
};
加了 __strong 后:
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // self 已死,整体不执行
[strongSelf doStep1]; // ✅ 安全
[strongSelf doStep2]; // ✅ 安全(strongSelf 临时持有,self 不会中途释放)
strongSelf.name = @"xx"; // ✅ 安全
// Block 执行完,strongSelf 出栈,临时强引用消失
// 不影响 self 的正常释放
};
关键理解:
__strong 创建的是一个临时的、局部的强引用
它只在 Block 执行期间生效
Block 不执行时,它不存在,不会造成循环引用
Block 执行时,它临时延长 self 的生命周期
Block 执行完,它随着栈帧销毁而消失
类比: Weak-Strong Dance 就像电影院的座位预留机制。__weak 是"不预留座位"------你来了有空位就坐,来晚了位子可能被撤了。__strong 是在进场(Block 开始执行)时确认一下"这个座位还在不在",如果在就暂时锁定它,看完电影(Block 执行完)自动解锁。
5.5 不是所有 Block 都会循环引用
scss
不会循环引用的场景:
├── UIView 动画 Block
│ └── [UIView animateWithDuration:animations:]
│ 系统持有 Block,Block 引用 self,但 self 不持有 Block
│ → 单向引用,不成环
│
├── GCD 一次性 Block
│ └── dispatch_async(queue, ^{ self.xxx; });
│ GCD 持有 Block(直到执行完),Block 引用 self
│ self 不持有 GCD 的 Block → 不成环
│ ⚠️ 但如果 dispatch_after 延时很长,self 的释放会被推迟
│
├── 局部变量 Block
│ └── void(^block)(void) = ^{ self.xxx; }; block();
│ block 是局部变量,函数结束即销毁 → 不成环
│
├── NSArray/NSDictionary 的 enumerate Block
│ └── [array enumerateObjectsUsingBlock:^{ self.xxx; }];
│ Block 同步执行完即释放 → 不成环
│
├── Masonry / SnapKit 的约束 Block
│ └── [view mas_makeConstraints:^{ make.top.equalTo(self.view); }];
│ Block 同步执行完即释放 → 不成环
│
└── 判断标准:
┌─────────────────────────────────────────────────┐
│ 画一条从 self 出发的"持有链" │
│ 如果能绕回 self → 循环引用 │
│ 如果不能绕回 self → 安全 │
│ │
│ self → 属性 → Block → self (环!) │
│ 系统 → Block → self (不成环,安全) │
│ 局部 → Block → self (不成环,安全) │
└─────────────────────────────────────────────────┘
5.6 循环引用的检测方法
markdown
检测循环引用的实用手段:
1. dealloc 日志法(最简单)
在类的 dealloc 方法中打印日志
如果页面退出后看不到日志 → 该对象没有被释放 → 可能存在循环引用
2. Instruments - Leaks
Xcode 自带工具,能自动检测泄漏的对象
可以看到泄漏对象的引用关系图
局限:不是所有循环引用都会被 Leaks 检测到
3. Instruments - Allocations(更可靠)
查看对象的生命周期(分配和释放历史)
如果一个对象只有 alloc 没有 dealloc → 泄漏
可以按类名过滤,非常方便
4. Memory Graph Debugger(推荐)
Xcode 调试时点击左下角的 Memory Graph 按钮
会以图形方式展示所有对象的引用关系
循环引用会被清晰地标注出来(紫色警告图标)
⭐ 最直观的检测方式
5. 第三方工具
MLeaksFinder(腾讯开源):自动检测 UIViewController 的泄漏
FBRetainCycleDetector(Facebook 开源):运行时检测循环引用
两者可以配合使用
6. Debug Memory Graph + lldb
在 Memory Graph 中选中可疑对象
在 lldb 中执行 po 命令查看对象详情
使用 malloc_history 命令追踪对象分配堆栈
排查思路流水线:
dealloc 没触发 → Memory Graph 看引用关系 → 找到环 → 分析哪个引用应该用 weak
六、Block 与内存管理的进阶话题
6.1 Block 的 copy 语义链
objectivec
Block copy 时发生的事情(连锁反应):
Block copy 到堆上
│
├── Block 结构体从栈拷贝到堆(malloc + memcpy)
│
├── 调用 descriptor->copy_helper
│ │
│ ├── 对捕获的 OC 对象执行 _Block_object_assign
│ │ ├── strong 对象 → retain(引用计数 +1)
│ │ ├── weak 对象 → objc_initWeak(注册弱引用)
│ │ └── block 对象 → 递归 _Block_copy
│ │
│ └── 对 __block 变量执行 _Block_object_assign
│ ├── __block 结构体从栈 copy 到堆
│ ├── 修改栈上 __forwarding 指向堆上副本
│ └── 对 __block 内部的 OC 对象执行相应的 retain/weak
│
└── 修改 isa 指针:_NSConcreteStackBlock → _NSConcreteMallocBlock
修改 flags:设置 BLOCK_NEEDS_FREE 位,引用计数初始化为 1
6.2 Block 的 dispose 语义链
scss
Block 引用计数归零时:
Block release → retainCount == 0
│
├── 调用 descriptor->dispose_helper
│ │
│ ├── 对捕获的 OC 对象执行 _Block_object_dispose
│ │ ├── strong 对象 → release(引用计数 -1)
│ │ ├── weak 对象 → objc_destroyWeak(注销弱引用)
│ │ └── block 对象 → 递归 _Block_release
│ │
│ └── 对 __block 变量执行 _Block_object_dispose
│ └── __block 结构体引用计数 -1,归零则:
│ ├── 对内部 OC 对象执行 release/destroyWeak
│ └── free(__block 结构体)
│
└── free(block) 释放 Block 堆内存
6.3 Block 的 retain/release 实现细节
rust
_Block_copy 的内部逻辑(简化版):
void *_Block_copy(const void *arg) {
struct Block_layout *src = (struct Block_layout *)arg;
if (src->flags & BLOCK_NEEDS_FREE) {
// 已经在堆上了 → 只增加引用计数
latching_incr_int(&src->flags); // flags 中的引用计数 +1
return src;
}
if (src->flags & BLOCK_IS_GLOBAL) {
// 全局 Block → 什么都不做,返回自身
return src;
}
// 栈上 Block → 拷贝到堆上
struct Block_layout *dst = malloc(src->descriptor->size);
memmove(dst, src, src->descriptor->size); // 整体内存拷贝
dst->isa = _NSConcreteMallocBlock; // 改 isa
dst->flags |= BLOCK_NEEDS_FREE; // 标记为堆 Block
dst->flags = (dst->flags & ~0xFFFF) | 1; // 引用计数 = 1
if (dst->flags & BLOCK_HAS_COPY_DISPOSE) {
dst->descriptor->copy_helper(dst, src); // 处理捕获变量
}
return dst;
}
性能洞察:
- Block copy 涉及 malloc + memmove + 可能的多次 retain
- 这就是为什么频繁创建和 copy Block 有性能开销
- 也是为什么 GCD 内部对 Block 的处理做了大量优化
6.4 Block 属性用 copy 还是 strong?
objectivec
MRC 时代:
@property (nonatomic, copy) void(^block)(void);
必须用 copy!
如果用 retain,Block 仍然在栈上,函数返回后 Block 失效 → 野指针 crash
copy 会把 Block 从栈拷贝到堆上,延长生命周期
ARC 时代:
@property (nonatomic, copy) void(^block)(void);
@property (nonatomic, strong) void(^block)(void);
两者效果完全相同!
ARC 编译器对 Block 赋值时自动插入 _Block_copy
无论你写 copy 还是 strong,底层都会执行 copy 操作
但惯例上仍然写 copy,原因:
① 代码自文档化------看到 copy 就知道"这是 Block,有特殊的内存语义"
② 向后兼容------万一哪天代码被挪到 MRC 环境也能正确工作
③ 团队共识------Apple 官方文档和社区都推荐 copy
┌─────────────────────────────────────────────────┐
│ ARC 下用 strong 也完全正确,但写 copy 更规范 │
└─────────────────────────────────────────────────┘
七、Block 与线程安全
7.1 Block 本身的线程安全性
scss
Block 对象一旦创建完成(copy 到堆上后),其内部状态是只读的。
invoke 指针、descriptor、捕获的变量值都不会再变。
因此:
✅ 多线程同时调用(invoke)同一个 Block → 安全(只读操作)
✅ 多线程同时对同一个 Block 做 retain/release → 安全
(引用计数操作是原子的,使用了 OSAtomicCompareAndSwapInt)
❌ 如果 Block 捕获了可变对象,多线程调用时对该对象的修改 → 不安全
┌──────────────────────────────────────────────────────────┐
│ Block 的"壳"是线程安全的,但"内容物"不一定是。 │
│ 就像一个上了锁的保险箱(Block)里面放了一把没有安全锁的刀 │
│ (NSMutableArray)------保险箱是安全的,但刀可以伤人。 │
└──────────────────────────────────────────────────────────┘
7.2 捕获变量的线程安全问题
objectivec
场景一:多个线程通过 Block 读写同一个 __block 变量
__block int counter = 0;
for (int i = 0; i < 1000; i++) {
dispatch_async(concurrentQueue, ^{
counter++; // 多线程同时 ++ → 数据竞争!结果不可预期
});
}
问题:counter++ 不是原子操作(读-改-写三步)
解决:用 dispatch_barrier_async 或 @synchronized 或 os_unfair_lock
场景二:Block 捕获的对象在另一个线程被释放
__weak typeof(self) weakSelf = self;
dispatch_async(bgQueue, ^{
// 此时 self 可能已经被主线程释放
[weakSelf doSomething]; // weakSelf 可能为 nil → 消息发给 nil,安全但无效
NSLog(@"%@", weakSelf.name); // 同理,可能返回 nil
});
这不是 crash,但可能导致逻辑不正确
→ 需要 Weak-Strong Dance
场景三:Block 中修改捕获的可变集合
NSMutableArray *arr = [NSMutableArray new];
dispatch_async(queue1, ^{ [arr addObject:@"A"]; });
dispatch_async(queue2, ^{ [arr addObject:@"B"]; });
两个 Block 捕获同一个 arr 指针(指向同一个可变数组)
同时修改 → crash(NSMutableArray 非线程安全)
解决:
① 使用串行队列保护
② 每个 Block 使用独立的 copy
③ 使用并发队列 + barrier
7.3 Block 与 GCD 的线程交互模式
ini
常见模式及其线程安全分析:
模式 1:主线程 → 后台 → 回主线程
dispatch_async(bgQueue, ^{
id result = [self heavyComputation]; // 后台线程
dispatch_async(dispatch_get_main_queue(), ^{
self.label.text = result; // 主线程更新 UI
});
});
线程安全性:
- heavyComputation 在后台线程执行 → 不能操作 UI
- result 是局部变量,被内层 Block 值捕获 → 安全
- 内层 Block 在主线程执行 → 可以操作 UI ✓
- 注意:self 被两层 Block 捕获 → 是否循环引用取决于 self 是否持有 queue
模式 2:dispatch_group 汇聚多个异步任务
dispatch_group_t group = dispatch_group_create();
__block NSArray *data1, *data2;
dispatch_group_async(group, queue, ^{ data1 = [self fetchData1]; });
dispatch_group_async(group, queue, ^{ data2 = [self fetchData2]; });
dispatch_group_notify(group, mainQueue, ^{
[self updateUIWithData1:data1 data2:data2]; // 两个任务都完成后
});
线程安全性:
- data1 和 data2 用 __block 修饰,多个 Block 共享同一个堆上变量
- 但因为两个 async 的 Block 各写各的变量 → 不冲突
- notify 的 Block 在两个 async 都完成后才执行 → 此时 data1/data2 已写入
- 安全 ✓(但如果多个 Block 写同一个变量就不安全了)
八、Block 在底层框架中的角色
8.1 GCD 中的 Block
scss
dispatch_async(queue, block)
执行流程:
│
├── 1. Block 被 copy 到堆上(GCD 内部调用 _Block_copy)
│
├── 2. Block 被封装进 dispatch_continuation_t 结构体
│ ┌──────────────────────────────┐
│ │ dispatch_continuation_t │
│ │ ├── do_vtable (虚表指针) │
│ │ ├── dc_func (执行函数) │
│ │ ├── dc_ctxt (Block 指针) │
│ │ ├── dc_voucher │
│ │ └── dc_priority │
│ └──────────────────────────────┘
│ 放入 queue 的 FIFO 链表
│
├── 3. 线程池中的 worker thread 取出 continuation
│ 调用 dc_func(dc_ctxt) → block->invoke(block)
│
└── 4. 执行完毕后 _Block_release(block)
Block 引用计数 -1,归零则触发 dispose 链
dispatch_sync 的差异:
- 同步调用不需要 copy Block(Block 在调用者栈上即可)
- 调用者线程阻塞等待,Block 在 queue 的线程上执行
- 执行完后调用者才继续,此时 Block 仍然有效
- 但要注意死锁:在串行队列中 dispatch_sync 到同一队列 → 死锁!
8.2 RunLoop 中的 Block
scss
CFRunLoopPerformBlock(runloop, mode, block)
│
├── Block 被 copy 到堆上
├── 挂载到 RunLoop 指定 mode 的 _blocks 链表
├── RunLoop 在对应 mode 的迭代中遍历链表执行
└── 执行后从链表移除并 release
RunLoop 与 Block 的生命周期关系:
- Block 提交后到执行前,一直被 RunLoop 持有
- 如果 RunLoop 切换到其他 mode,Block 不会执行
- 如果 RunLoop 退出,未执行的 Block 会被 release
performSelector:withObject:afterDelay: 的底层也是 RunLoop + Timer + Block
8.3 Notification/KVO 中的 Block
objectivec
id token = [[NSNotificationCenter defaultCenter]
addObserverForName:@"xxx"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
// 这个 Block 被 NotificationCenter 持有
// 直到 removeObserver 才释放
}];
生命周期陷阱:
NotificationCenter → observer(内部对象) → Block → self
↑
如果 self 持有 token 并在 dealloc 中 removeObserver:
- self 的 dealloc 永远不会调用(因为 Block 强引用 self)
- token 永远不会被 remove
→ 经典死锁式内存泄漏
解决:Block 内必须用 weakSelf
8.4 Block 作为 Associated Object
scss
objc_setAssociatedObject(self, key, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
- COPY 策略会调用 _Block_copy
- Block 被关联到对象上,对象释放时 Block 才释放
- 如果 Block 捕获了该对象 → 循环引用!
- 这是很多第三方库(如方法交换添加 Block 回调)的常见泄漏原因
8.5 Block 在 KVO 中的新 API
objectivec
iOS 11+ 提供了基于 Block 的 KVO API:
self.observation = [self.model observe:@selector(name)
options:NSKeyValueObservingOptionNew
changeHandler:^(Model *model, ...) {
// 注意:Block 参数直接给了被观察对象,不需要 self
// Apple 有意设计成不需要捕获 self
NSLog(@"%@", model.name);
}];
self.observation 持有 observation token
observation token 持有 Block
Block 引用 model(不是 self)
→ 不形成循环引用
→ observation token 在 self dealloc 时被释放
→ 自动移除观察者
这个设计是 Apple 总结了无数 KVO 内存泄漏问题后的改良方案。
九、Block 的性能考量
9.1 Block 的开销分析
scss
Block 的性能开销来源:
1. 创建开销
├── 栈 Block:几乎为零(只是栈指针移动 + 结构体初始化)
├── 堆 Block:malloc + memcpy + 可能的多次 retain
└── 全局 Block:零开销(编译期确定)
2. 调用开销
├── 通过函数指针间接调用(和 C 函数指针相同)
├── 比 OC 方法调用快(没有 objc_msgSend 的查找过程)
├── 比直接函数调用慢(多一次指针解引用)
└── 和 C++ 虚函数调用类似的性能级别
3. 销毁开销
├── 栈 Block:零(栈帧弹出即可)
├── 堆 Block:dispose_helper + free + 可能的多次 release
└── 全局 Block:永不销毁
性能对比(从快到慢):
直接函数调用 ≈ 内联 Block
> C 函数指针调用 ≈ Block 调用
> objc_msgSend(方法调用)
> performSelector
9.2 编译器对 Block 的优化
scss
编译器在开启优化时(-O1 及以上)会对 Block 做以下优化:
1. 内联优化
如果 Block 在定义后立即调用且只使用一次
编译器可能将其内联,消除 Block 开销
2. 栈提升为全局
如果 Block 不捕获变量,即使写在函数内部
编译器也会将其提升为 GlobalBlock
3. copy 消除
如果编译器能证明 Block 不会逃逸出当前作用域
可能跳过 copy 操作
4. 捕获变量合并
多个 Block 捕获相同变量时,编译器可能优化内存布局
9.3 大量 Block 场景的性能优化建议
ini
场景:高频回调(如滚动监听、动画帧回调)
问题:
每次回调都创建新 Block → 频繁 malloc/free
Block 捕获大量对象 → 频繁 retain/release
优化策略:
① 复用 Block:将 Block 保存为属性,避免重复创建
// Bad:每次滚动都创建新 Block
scrollView.didScroll = ^{ [self handleScroll]; };
// Good:初始化时创建一次
self.scrollHandler = ^{ [weakSelf handleScroll]; };
scrollView.didScroll = self.scrollHandler;
② 减少捕获变量:只捕获真正需要的变量
// Bad:隐式捕获整个 self(包含所有 ivar 的引用)
^{ _array = ...; _dict = ...; }
// Good:只传入需要的对象
NSMutableArray *arr = _array;
^{ [arr addObject:...]; }
③ 考虑用函数指针替代 Block(极致性能场景)
在 C 层面的高频回调中,函数指针比 Block 更轻量
因为没有结构体创建、copy、dispose 的开销
④ 使用 dispatch_block_create 的 DISPATCH_BLOCK_NO_QOS_CLASS 标志
避免 QoS 传播的额外开销
十、Block 的调试技巧
10.1 在运行时识别 Block 类型
scss
调试时经常需要确认 Block 的类型和捕获信息:
lldb 命令:
(lldb) po block
→ 输出 Block 的描述,包含类型信息
(lldb) po [block class]
→ __NSGlobalBlock__ / __NSStackBlock__ / __NSMallocBlock__
(lldb) po [block superclass]
→ NSBlock
(lldb) memory read --size 8 --count 5 (void *)block
→ 读取 Block 结构体的前 5 个字段(isa, flags, reserved, invoke, descriptor)
(lldb) po (void *)((void **)block)[3]
→ 读取 invoke 函数指针地址
(lldb) image lookup -a <invoke 地址>
→ 反查 invoke 函数对应的源代码位置
通常输出类似:__ClassName_methodName_block_invoke
10.2 在汇编层面识别 Block 调用
scss
Block 调用在 ARM64 汇编中的特征:
Block 创建:
通常会看到 __copy_helper_block_ 和 __destroy_helper_block_ 的引用
以及 ___block_descriptor_ 相关的符号
Block 调用:
ldr x8, [x0, #16] // 从 Block 结构体偏移 16 字节处加载 invoke 指针
blr x8 // 跳转到 invoke 函数执行
↑ 这两条指令是 Block 调用的标志性模式
x0 既是 Block 指针,也作为 invoke 的第一个参数(隐含 self)
Block 捕获变量访问:
在 invoke 函数内部,通过 x0(Block 指针)+ 偏移量 来访问捕获的变量
ldr x8, [x0, #32] // 访问第一个捕获变量(偏移量取决于结构体布局)
10.3 排查 Block 相关的 Crash
scss
常见 Block Crash 类型及排查方法:
1. EXC_BAD_ACCESS ------ 调用已释放的 Block
原因:栈 Block 在栈帧销毁后被调用
特征:crash 在 block->invoke(block, ...) 处
排查:检查 Block 是否被正确 copy 到堆上
MRC 下尤其常见
2. EXC_BAD_ACCESS ------ Block 内访问已释放的对象
原因:Block 用 __unsafe_unretained 捕获了一个已释放的对象
特征:crash 在 Block 内部的 objc_msgSend 处
排查:将 __unsafe_unretained 改为 __weak
3. Block 为 nil 时调用 → 直接 crash
原因:Block 指针为 nil 时,调用会触发 EXC_BAD_ACCESS
因为底层是 block->invoke(block),nil 解引用
特征:crash 地址通常是 0x10 附近(nil + invoke 偏移量)
排查:调用前判空 → if (block) { block(); }
┌──────────────────────────────────────────────────┐
│ OC 方法调用可以安全地发给 nil → [nil doSomething] │
│ Block 调用不能发给 nil → nil() 会 crash! │
│ 这是一个容易被忽略的差异。 │
└──────────────────────────────────────────────────┘
4. 野 Block(Block 指针指向已被回收的内存)
原因:使用 __unsafe_unretained 接收 Block,Block 被释放后指针未置 nil
特征:crash 地址随机,表现不稳定
排查:不要用 __unsafe_unretained 存储 Block
10.4 使用 clang 查看 Block 编译后的 C++ 代码
scss
在终端中执行以下命令,可以看到 Block 被编译器转换后的 C++ 代码:
clang -rewrite-objc main.m -o main.cpp
这会把所有 Block 转换为对应的结构体和函数,帮助你理解底层原理。
输出中你会看到:
- __main_block_impl_0 结构体(Block 的结构体)
- __main_block_func_0 函数(Block 的 invoke 函数)
- __main_block_desc_0 结构体(Block 的 descriptor)
- __Block_byref_xxx 结构体(__block 变量的结构体)
注意:
这个命令生成的代码是简化版的,和 ARC 实际编译结果有差异。
它主要用于学习和理解原理,不是 100% 精确的编译输出。
如果需要精确的汇编输出,使用 Xcode 的 Product → Perform Action → Assemble。
十一、Block 与 Swift 的桥接
11.1 OC Block 在 Swift 中的映射
objectivec
OC Block 类型与 Swift 闭包类型的对应关系:
OC: void(^)(void) → Swift: () -> Void
OC: void(^)(int, NSString *) → Swift: (Int32, String) -> Void
OC: NSString *(^)(int) → Swift: (Int32) -> String
OC: void(^)(BOOL *stop) → Swift: (UnsafeMutablePointer<ObjCBool>) -> Void
桥接规则:
① OC 的 Block 类型自动桥接为 Swift 的 @convention(block) 闭包
② 参数和返回值类型按照 Swift-OC 桥接规则转换
③ nullable Block 映射为 Optional 闭包
在 Swift 中使用 OC Block API:
OC 定义:
- (void)fetchDataWithCompletion:(void(^)(NSArray *data, NSError *error))completion;
Swift 调用:
obj.fetchData { data, error in
// data 是 [Any]?,error 是 Error?
}
逃逸标注的影响:
OC 中标注了 NS_NOESCAPE 的 Block 参数 → Swift 中映射为非逃逸闭包
OC 中未标注的 Block 参数 → Swift 中映射为 @escaping 闭包
这意味着:
如果 OC API 的 Block 参数是同步使用的,应该标注 NS_NOESCAPE
这样 Swift 调用者不需要写 self.(非逃逸闭包不捕获 self 的强引用)
11.2 Swift 闭包在 OC 中的使用
kotlin
Swift 闭包可以桥接到 OC Block,但有限制:
可以桥接的:
✅ 不捕获泛型类型的闭包
✅ 使用 @convention(block) 标注的闭包
✅ 返回值和参数都是 OC 可表达类型
不能桥接的:
❌ 捕获了 Swift 特有类型(如 struct、enum with associated values)
❌ 使用了 Swift only 的特性(如 throws、async)
@convention(block) 的作用:
告诉 Swift 编译器:"把这个闭包按照 OC Block 的 ABI 来生成"
而不是 Swift 原生闭包的 ABI
let block: @convention(block) (Int) -> String = { num in
return "\(num)"
}
这个闭包现在是一个合法的 OC Block 对象
可以传给任何接受 Block 的 OC API
11.3 Swift 闭包 vs OC Block 的关键差异
swift
┌───────────────────┬─────────────────────┬────────────────────────┐
│ 维度 │ OC Block │ Swift 闭包 │
├───────────────────┼─────────────────────┼────────────────────────┤
│ 默认捕获方式 │ 值捕获 │ 引用捕获 │
│ 修改外部变量 │ 需要 __block │ 默认就可以 │
│ 存储位置 │ 栈/堆/全局 │ 堆上(编译器优化除外) │
│ 逃逸标注 │ 无(手动注意) │ @escaping 编译器强制 │
│ 循环引用处理 │ weakSelf/strongSelf │ [weak self] 捕获列表 │
│ nil 安全 │ nil Block 调用 crash │ Optional 闭包安全 │
│ 类型系统 │ 弱类型(仅 runtime) │ 强类型(编译期检查) │
│ 内存管理 │ ARC + 手动 copy │ 纯 ARC │
└───────────────────┴─────────────────────┴────────────────────────┘
Swift 的设计吸取了 OC Block 的教训:
- 默认引用捕获 → 不需要 __block 的心智负担
- @escaping 强制标注 → 编译期就发现逃逸问题
- 闭包直接在堆上 → 没有栈→堆迁移的复杂性
- 捕获列表 [weak self] → 比 weakSelf/strongSelf 更简洁
- Optional 闭包 → nil 安全,不会 crash
十二、Block 的常见坑与反模式
12.1 十大常见 Block 陷阱
scss
陷阱 1:忘记 Block 可以为 nil
self.completionHandler(result);
// 如果 completionHandler 为 nil → crash!
正确做法:
if (self.completionHandler) {
self.completionHandler(result);
}
─────────────────────────────────────────
陷阱 2:在 dealloc 中依赖 weakSelf
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf cleanup]; // dealloc 流程中 weakSelf 可能已经是 nil!
};
dealloc 时 weak 引用可能已经被清零(取决于时机)
─────────────────────────────────────────
陷阱 3:Block 内创建局部 strong 引用后以为不会循环引用
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.anotherBlock = ^{
[strongSelf doSomething]; // strongSelf 是局部变量,但被内层 Block 捕获
// self → block → 外层 Block → strongSelf 被内层 Block 捕获为强引用
// 内层 Block 被赋值给 self.anotherBlock → 形成循环引用!
};
};
Weak-Strong Dance 只保护一层,嵌套 Block 需要再次 weak!
─────────────────────────────────────────
陷阱 4:在栈 Block 出作用域后使用(MRC)
void(^block)(void);
{
int x = 42;
block = ^{ NSLog(@"%d", x); };
}
block(); // crash!Block 在栈上,出作用域后失效
─────────────────────────────────────────
陷阱 5:误以为 copy 会创建独立副本
void(^block)(void) = ^{ NSLog(@"hello"); };
void(^block2)(void) = [block copy];
// block2 == block(堆上 Block copy 只增加引用计数)
// 不像 NSMutableArray copy 会创建新对象
─────────────────────────────────────────
陷阱 6:dispatch_after 的 Block 延长了对象生命周期
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
[self doSomething]; // self 至少活到 30 秒后
});
用户关闭页面后,ViewController 30 秒内不会被释放
这不是"循环引用",但是"生命周期延长"
─────────────────────────────────────────
陷阱 7:在 Block 内使用 C 数组
int arr[3] = {1, 2, 3};
void(^block)(void) = ^{
NSLog(@"%d", arr[0]); // 编译错误!C 数组不能被 Block 捕获
};
C 数组不是一等公民,不能被值拷贝
解决:使用 __block 修饰,或者用 NSArray/指针代替
─────────────────────────────────────────
陷阱 8:同步 Block 中的 return 语义
- (BOOL)check {
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if ([obj isKindOfClass:[NSString class]]) {
return; // 这个 return 只退出 Block,不退出 check 方法!
}
}];
return NO; // 无论如何都会执行到这里
}
Block 内的 return 退出的是 Block,不是外层方法
─────────────────────────────────────────
陷阱 9:多次调用一次性 Block
if (self.completion) {
self.completion(result);
self.completion(result); // 二次调用!可能导致重复操作
}
最佳实践:调用后立即置 nil
if (self.completion) {
void(^completion)(id) = self.completion;
self.completion = nil;
completion(result);
}
─────────────────────────────────────────
陷阱 10:忽略 Block 的 copy 开销(性能敏感场景)
for (int i = 0; i < 10000; i++) {
self.handler = ^{ ... }; // 每次循环都创建新 Block 并 copy 到堆上
}
在热路径中频繁创建和赋值 Block 可能导致性能问题
解决:Block 不变时在循环外创建一次
12.2 Block 的最佳实践总结
scss
✅ DO(推荐做法):
1. Block 属性用 copy(即使 ARC 下 strong 等效,copy 更清晰)
2. 使用 Weak-Strong Dance 处理循环引用
3. 调用 Block 前检查是否为 nil
4. Completion Block 调用后置 nil
5. 使用 typedef 为复杂 Block 类型定义别名
6. API 中的 Block 参数注明是否逃逸(NS_NOESCAPE)
7. 嵌套 Block 中的每一层都检查循环引用
❌ DON'T(避免做法):
1. 不要在 Block 内直接访问 ivar(容易隐式捕获 self)
2. 不要用 __unsafe_unretained 代替 __weak(野指针风险)
3. 不要在 ARC 下用 __block 来打破循环引用(ARC 下 __block 会 retain)
4. 不要在不确定是否为 nil 的情况下直接调用 Block
5. 不要在高频回调中频繁创建新 Block
6. 不要在 Block 内做耗时操作而不考虑线程
7. 不要忘记 NSTimer/NotificationCenter 的 Block 生命周期管理
十三、Block vs Delegate vs Notification ------ 如何选择
markdown
三种回调机制的对比:
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 维度 │ Block │ Delegate │ Notification │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 关系 │ 一对一 │ 一对一 │ 一对多 │
│ 耦合度 │ 高(代码内联)│ 低(协议隔离)│ 最低(解耦) │
│ 代码位置 │ 就地书写 │ 分散在方法中 │ 分散 + 跨文件│
│ 类型安全 │ 中 │ 高 │ 低 │
│ 内存风险 │ 循环引用 │ 较少 │ 忘记 remove │
│ 适合场景 │ 一次性回调 │ 多方法协议 │ 广播通知 │
│ 调试难度 │ 较容易 │ 中 │ 难追踪 │
└──────────────┴──────────────┴──────────────┴──────────────┘
选择建议:
单一回调(如网络请求完成)→ Block
优势:代码集中,上下文清晰
例:[api fetchDataWithCompletion:^(Data *d) { ... }];
多个回调方法(如 UITableView 的数据源)→ Delegate
优势:职责明确,方法独立,可选实现
例:UITableViewDataSource 有多个 required/optional 方法
一对多广播(如登录状态变化通知所有页面)→ Notification
优势:完全解耦,任意对象可监听
例:用户登出后通知所有页面刷新
混合使用:实际项目中经常组合使用
例:网络层用 Block 回调给业务层,
业务层通过 Notification 广播给 UI 层
十四、知识体系脑图
objectivec
Block
├── 本质
│ ├── C 语言扩展(Clang 实现)
│ ├── 匿名函数 + 上下文捕获
│ ├── 底层是 OC 对象(有 isa)
│ ├── 编译期变换:源码 → 结构体 + 函数
│ └── 默认值捕获的设计哲学(Pit of Success)
│
├── 底层结构
│ ├── Block_layout 结构体
│ │ ├── isa → 类型标识(Global/Stack/Malloc)
│ │ ├── flags → 位域(引用计数 + 多种标志)
│ │ ├── invoke → 函数指针(隐含 self 参数)
│ │ └── descriptor → 多态结构(size/copy/dispose/signature)
│ ├── 捕获变量存储在结构体尾部(动态大小)
│ └── 类型编码(Type Encoding)与 NSMethodSignature
│
├── 三种类型
│ ├── GlobalBlock(不捕获局部变量,在数据段)
│ ├── StackBlock(捕获变量,在栈上,生命周期同栈帧)
│ ├── MallocBlock(copy 后在堆上,引用计数管理)
│ ├── 为什么有栈?性能优化,避免不必要的 malloc
│ └── 逃逸与非逃逸的概念
│
├── 变量捕获(核心难点)
│ ├── 局部基本类型 → 值拷贝(快照语义)
│ ├── 局部对象 → 指针拷贝 + ARC 内存管理(strong/weak)
│ ├── __block → 封装为 __Block_byref 结构体
│ │ ├── __forwarding 保证栈堆访问一致性
│ │ ├── 多 Block 共享时引用计数管理
│ │ └── MRC vs ARC 下对对象的不同行为
│ ├── 静态变量 → 指针捕获(地址不变,无需拷贝值)
│ ├── 全局变量 → 不捕获(直接按地址访问)
│ └── self 的隐式捕获(访问 ivar 也会捕获 self)
│
├── 循环引用
│ ├── 本质:强引用环导致引用计数无法归零
│ ├── 直接环 vs 间接环(多层持有链)
│ ├── __weak(打破强引用)
│ ├── __weak + __strong(Weak-Strong Dance,保证执行完整性)
│ ├── NSTimer 的特殊循环引用
│ ├── 判断标准:从 self 出发能否画回 self
│ └── 检测方法(Memory Graph / Instruments / MLeaksFinder)
│
├── 内存管理
│ ├── copy 链:malloc → memcpy → copy_helper → retain/weak/递归copy
│ ├── dispose 链:dispose_helper → release/destroyWeak → free
│ ├── 引用计数存储在 flags 位域中(非 SideTable)
│ ├── ARC 下编译器自动 copy(5 种场景)
│ └── copy vs strong 属性修饰符的选择
│
├── 线程安全
│ ├── Block 对象本身的线程安全性(只读 + 原子引用计数)
│ ├── 捕获变量的线程安全问题
│ ├── __block 变量的数据竞争
│ └── GCD + Block 的常见线程安全模式
│
├── 性能
│ ├── 创建:栈≈0 / 堆=malloc+copy / 全局=0
│ ├── 调用:比 objc_msgSend 快,比直接调用慢一点
│ ├── 销毁:栈=0 / 堆=dispose+free
│ ├── 编译器优化:内联、全局提升、copy 消除
│ └── 高频场景的优化建议
│
├── 框架应用
│ ├── GCD(dispatch_continuation_t 封装)
│ ├── RunLoop(Block 链表管理)
│ ├── Notification/KVO(Block 生命周期陷阱)
│ ├── Associated Object(COPY 策略注意循环引用)
│ └── KVO 新 API 的设计改进
│
├── 调试技巧
│ ├── lldb 命令查看 Block 类型和捕获信息
│ ├── 汇编层面识别 Block 调用
│ ├── 常见 Block Crash 类型及排查
│ └── clang -rewrite-objc 查看编译产物
│
├── Swift 桥接
│ ├── OC Block → Swift 闭包的类型映射
│ ├── @convention(block) 的作用
│ ├── NS_NOESCAPE 对 Swift 侧的影响
│ └── Swift 闭包 vs OC Block 的关键差异
│
├── 常见坑与反模式
│ ├── nil Block 调用 crash
│ ├── 嵌套 Block 的循环引用
│ ├── dispatch_after 延长对象生命周期
│ ├── Block 内 return 的语义
│ ├── C 数组不能被 Block 捕获
│ └── 一次性 Block 多次调用
│
└── 设计选择
├── Block vs Delegate vs Notification 的取舍
└── Block 回调的最佳实践
十五、面试高频考点速查
| 问题 | 核心答案 |
|---|---|
| Block 的本质是什么? | 封装了函数指针和捕获变量的 OC 对象(结构体),有 isa 指针 |
| Block 的 invoke 函数有什么特点? | 第一个隐含参数是 Block 结构体自身指针,类似 OC 的 self |
| Block 有几种类型?怎么判定? | Global(不捕获局部变量)、Stack(捕获了,在栈上)、Malloc(copy 后在堆上) |
| 什么时候 Block 会从栈 copy 到堆? | 作为返回值、赋值给 strong 变量、传给 GCD、ARC 编译器自动处理 |
| 为什么要有栈 Block? | 性能优化,临时 Block 无需 malloc/free,生命周期随栈帧 |
| Block 的引用计数存在哪里? | flags 字段的 bit 1~15,不在 SideTable 中 |
| 为什么局部变量在 Block 内不能修改? | 捕获的是值副本,修改副本无意义且语义混乱,编译器直接禁止 |
__block 的底层原理? |
将变量封装为 __Block_byref 堆上结构体,通过 __forwarding 指针保证栈堆访问一致 |
__forwarding 为什么存在? |
解决 Block copy 后栈上代码和堆上 Block 访问同一个 __block 变量的一致性问题 |
多个 Block 捕获同一个 __block 变量会怎样? |
共享同一个堆上 __Block_byref 结构体,引用计数管理 |
__block 在 MRC 和 ARC 下有什么区别? |
MRC 下不 retain 对象(可打破循环引用),ARC 下会 retain(不能打破循环引用) |
| 访问 ivar 会捕获 self 吗? | 会,编译器将 _name 转为 self->_name,隐式强引用捕获 self |
| 循环引用怎么产生的? | self 持有 Block,Block 强引用捕获 self,形成引用环 |
__weak 和 __unsafe_unretained 区别? |
weak 对象释放后自动置 nil;unsafe_unretained 不置 nil,可能野指针 |
| Weak-Strong Dance 的意义? | weak 避免循环引用,strong 保证 Block 执行期间 self 不被中途释放 |
| Block 捕获 self 一定循环引用吗? | 不一定,只有 self(直接或间接)持有 Block 时才会形成环 |
| Block 和 C 函数指针的区别? | Block 能捕获上下文,是 OC 对象,参与 ARC;函数指针都不能 |
| Block 和 Swift 闭包的主要区别? | Block 默认值捕获,Swift 默认引用捕获;Block 有栈→堆迁移,Swift 闭包直接在堆上 |
| Block 调用比 objc_msgSend 快还是慢? | 快,Block 通过函数指针直接调用,省去了方法查找(SEL→IMP)的过程 |
| Block 为 nil 时调用会怎样? | Crash(EXC_BAD_ACCESS),不像 OC 消息发送给 nil 是安全的 |
_Block_object_assign 做了什么? |
根据 flags 对捕获变量做 retain/initWeak/递归copy/byref_copy 等操作 |
| GCD Block 需要 weakSelf 吗? | 通常不需要,因为 self 不持有 GCD Block,不成环。但长延时的 dispatch_after 会推迟 self 释放 |
| Block 的 copy 属性在 ARC 下还有意义吗? | 功能上 strong 等效,但 copy 更具自文档性,是社区推荐写法 |
| 如何检测循环引用? | Xcode Memory Graph Debugger、Instruments Leaks/Allocations、MLeaksFinder |
| 什么是逃逸 Block? | 超出创建它的函数作用域后仍可能被调用的 Block,必须 copy 到堆上 |
| NSTimer 为什么容易循环引用? | NSTimer 强引用 target,RunLoop 强引用 timer,即使 self 不强引用 timer,仍可能泄漏 |
| Block 线程安全吗? | Block 对象本身线程安全(只读),但捕获的可变对象的操作不是线程安全的 |