2-22.【OC】【内存管理】画出一个“完整的循环引用全景图”:要求包含:ViewController、Block、Timer、Delegate、RunLoop

7 阅读2分钟

这是一个非常经典且复杂的内存拓扑结构。在 iOS 开发中,这五个角色往往交织在一起,形成一个“牵一发而动全身”的引用网络。

为了让你看清它们的真实关系,我们把这张图拆解为两条核心路径:一条是“显性”的(你写的代码),另一条是“隐性”的(系统底层的运行机制)。


完整的循环引用全景图

1. 路径一:ViewController 与 Block/Delegate 的博弈

这是最常见的业务层循环:

  • ViewController strong\xrightarrow{strong} Block:你通常会定义一个属性来保存回调闭包。
  • Block strong\xrightarrow{strong} ViewController:如果你在 Block 内部直接调用了 self(且没有使用 [weak self]),Block 的结构体会强行捕获并 retain ViewController。
  • ViewController strong\xrightarrow{strong} View/Component weak\xrightarrow{weak} Delegate:这是正确的打破方式。但如果开发者误将 delegate 设为 strong,就会产生 ViewController \leftrightarrow Component 的死锁。

2. 路径二:Timer 与 RunLoop 的“暗箱操作”

这是最具迷惑性的路径,因为它引入了外部系统常驻对象:

  • RunLoop (常驻线程) strong\xrightarrow{strong} NSTimer:一旦 Timer 注册到 RunLoop(如 scheduledTimer...),它就被 RunLoop 深度持有了。
  • NSTimer strong\xrightarrow{strong} ViewController (Target) :即使你给 Timer 传的是 weakSelf,旧版的 Timer 内部依然会强引用这个 target
  • ViewController strong\xrightarrow{strong} NSTimer:如果你又用一个属性存了这个 Timer,闭环达成。

核心节点解剖表

节点角色关键特性崩溃/泄露风险点
RunLoop顶层持有者线程存活它就存活只要它不放开 Timer,后面的链条都无法断开
ViewController逻辑中枢业务生命周期的核心往往是循环引用的起点和终点
Block匿名函数默认强捕获所有外部变量捕获了 self 却没有 weak 处理
NSTimer外部触发器自动持有 Target依赖 dealloc 来停止,但被强引用导致无法进入 dealloc
Delegate通讯协议通常应为弱引用误设为 strong 导致父子对象互锁

为什么这个图里 dealloc 永远不会执行?

观察上面的全景图,你会发现一个死循环逻辑

  1. 用户点击“返回”,试图销毁 ViewController
  2. ViewController 准备销毁,但发现 Timer 还抓着它。
  3. Timer 说:“我只有在执行 invalidate 之后才会放开你”。
  4. 开发者说:“我把 invalidate 写在 ViewControllerdealloc 里了”。
  5. 系统说:“因为 Timer 不放手,ViewController 进不去 dealloc,所以 invalidate 永远不会执行”。

结果: RunLoop \to Timer \to ViewController 这一整条链条全部驻留在内存中。


💡 终极修复方案

要打破这个全景图里的所有环,你需要:

  1. Block:内部使用 [weak self]

  2. Delegate:属性声明必须为 weak

  3. Timer

    • 使用 iOS 10+ 的 Block 版本
    • 或者在 viewWillDisappear: 中手动调用 [timer invalidate](不等 dealloc)。
    • 或者引入一个 NSProxy 作为中间人(断开 Timer 对 ViewController 的强引用)。