这是一个非常深入的切入点。虽然两者在表现上都会导致“循环引用”,但在 ARC(自动引用计数) 的底层处理逻辑和“谁在持有谁”的权力结构上,有着本质的区别。
1. 相似之处:强引用的结果
无论是 Timer 还是 Block(闭包),它们的共同点在于:默认都会对捕获的对象(通常是 self)的引用计数 。
- 目的一致:为了确保在异步执行或延时执行时,目标对象不会因为提前释放而导致野指针崩溃。
- 风险一致:如果
self也强持有这个Timer或Block,就会形成闭包环,导致内存泄漏。
2. 核心不同:谁发起的强引用?
这是理解两者语义差异的关键。
A. Block 的捕获:编译器行为
当你在代码中写下 ^{ ... self.property ... } 时,编译器在编译阶段会生成一个结构体。
- 规则:编译器会自动检测 Block 内部使用的外部变量。如果是强指针,Block 结构体内部会增加一个相同类型的强指针成员变量。
- 持有者:Block 对象本身直接持有了
self。
B. Timer 的捕获:运行时行为
NSTimer 对 target 的捕获并不是编译器层面的变量捕获,而是** API 内部的函数实现**。
- 规则:当你调用
initWithTarget:self时,NSTimer的内部初始化代码会显式地对这个id对象调用一次retain。 - 持有者:Timer 内部的实例变量持有了
self。
3. 语义维度的深层差异
| 维度 | Block 捕获 (self) | Timer 捕获 (target: self) |
|---|---|---|
| 生效阶段 | 编译期(静态分析) | 运行期(动态赋值) |
| 弱化手段 | __weak 或 [weak self] 有效 | __weak 对旧版 API 无效 |
| 引用链条 | self Block self | RunLoop Timer self |
| 解除条件 | Block 执行完或被置为 nil | 必须显式调用 invalidate |
4. 为什么 __weak 对旧版 Timer 无效?
这是最容易产生误区的地方:
- Block 情况:如果你传入
__weak typeof(self) weakSelf = self;,Block 结构体内部存的就是一个 弱指针。当self释放时,这个指针自动变nil。 - Timer 情况:旧版 Timer 的
target参数类型是(id)target。即使你传入一个弱指针,NSTimer内部依然会通过这个指针找到对象的内存地址,然后强行执行一次retain。它并不识别你传入的是“弱引用变量”还是“强引用变量”,它只看对象本身。
这就是为什么针对 Timer 的
weakSelf优化,必须通过“中介者”(Proxy)或者使用 iOS 10+ 的 Block 版本 API 才能生效。
5. 解除引用的“触发机制”不同
- Block:如果是单次执行的 Block(如
UIView动画或网络请求),执行完毕后,Block 通常会被销毁或释放其捕获列表,引用计数自动回落。 - Timer:只要
repeats为YES,且没有调用invalidate,Timer 就永远不会主动释放对self的强引用。它是一个持久态的强持有。
💡 结论
- Block 捕获像是一个**“快照”**:编译器帮你把变量存进了一个包裹。
- Timer 强引用像是一个**“契约”**:RunLoop 只要在跑,Timer 就必须保住
target的命。