7-14.【OC】【RunLoop】Timer 捕获 self 的 ARC 语义与 Block 捕获规则有什么相似或不同?

2 阅读3分钟

这是一个非常深入的切入点。虽然两者在表现上都会导致“循环引用”,但在 ARC(自动引用计数) 的底层处理逻辑和“谁在持有谁”的权力结构上,有着本质的区别。


1. 相似之处:强引用的结果

无论是 Timer 还是 Block(闭包),它们的共同点在于:默认都会对捕获的对象(通常是 self)的引用计数 +1+1

  • 目的一致:为了确保在异步执行或延时执行时,目标对象不会因为提前释放而导致野指针崩溃。
  • 风险一致:如果 self 也强持有这个 TimerBlock,就会形成闭包环,导致内存泄漏。

2. 核心不同:谁发起的强引用?

这是理解两者语义差异的关键。

A. Block 的捕获:编译器行为

当你在代码中写下 ^{ ... self.property ... } 时,编译器在编译阶段会生成一个结构体。

  • 规则:编译器会自动检测 Block 内部使用的外部变量。如果是强指针,Block 结构体内部会增加一个相同类型的强指针成员变量。
  • 持有者Block 对象本身直接持有了 self

B. Timer 的捕获:运行时行为

NSTimertarget 的捕获并不是编译器层面的变量捕获,而是** API 内部的函数实现**。

  • 规则:当你调用 initWithTarget:self 时,NSTimer 的内部初始化代码会显式地对这个 id 对象调用一次 retain
  • 持有者Timer 内部的实例变量持有了 self

3. 语义维度的深层差异

维度Block 捕获 (self)Timer 捕获 (target: self)
生效阶段编译期(静态分析)运行期(动态赋值)
弱化手段__weak[weak self] 有效__weak 对旧版 API 无效
引用链条self \rightarrow Block \rightarrow selfRunLoop \rightarrow Timer \rightarrow 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:只要 repeatsYES,且没有调用 invalidate,Timer 就永远不会主动释放对 self 的强引用。它是一个持久态的强持有。

💡 结论

  • Block 捕获像是一个**“快照”**:编译器帮你把变量存进了一个包裹。
  • Timer 强引用像是一个**“契约”**:RunLoop 只要在跑,Timer 就必须保住 target 的命。