这是一个非常经典且复杂的内存拓扑结构。在 iOS 开发中,这五个角色往往交织在一起,形成一个“牵一发而动全身”的引用网络。
为了让你看清它们的真实关系,我们把这张图拆解为两条核心路径:一条是“显性”的(你写的代码),另一条是“隐性”的(系统底层的运行机制)。
完整的循环引用全景图
1. 路径一:ViewController 与 Block/Delegate 的博弈
这是最常见的业务层循环:
- ViewController Block:你通常会定义一个属性来保存回调闭包。
- Block ViewController:如果你在 Block 内部直接调用了
self(且没有使用[weak self]),Block 的结构体会强行捕获并retainViewController。 - ViewController View/Component Delegate:这是正确的打破方式。但如果开发者误将
delegate设为strong,就会产生 ViewController Component 的死锁。
2. 路径二:Timer 与 RunLoop 的“暗箱操作”
这是最具迷惑性的路径,因为它引入了外部系统常驻对象:
- RunLoop (常驻线程) NSTimer:一旦 Timer 注册到 RunLoop(如
scheduledTimer...),它就被 RunLoop 深度持有了。 - NSTimer ViewController (Target) :即使你给 Timer 传的是
weakSelf,旧版的 Timer 内部依然会强引用这个target。 - ViewController NSTimer:如果你又用一个属性存了这个 Timer,闭环达成。
核心节点解剖表
| 节点 | 角色 | 关键特性 | 崩溃/泄露风险点 |
|---|---|---|---|
| RunLoop | 顶层持有者 | 线程存活它就存活 | 只要它不放开 Timer,后面的链条都无法断开 |
| ViewController | 逻辑中枢 | 业务生命周期的核心 | 往往是循环引用的起点和终点 |
| Block | 匿名函数 | 默认强捕获所有外部变量 | 捕获了 self 却没有 weak 处理 |
| NSTimer | 外部触发器 | 自动持有 Target | 依赖 dealloc 来停止,但被强引用导致无法进入 dealloc |
| Delegate | 通讯协议 | 通常应为弱引用 | 误设为 strong 导致父子对象互锁 |
为什么这个图里 dealloc 永远不会执行?
观察上面的全景图,你会发现一个死循环逻辑:
- 用户点击“返回”,试图销毁 ViewController。
- ViewController 准备销毁,但发现 Timer 还抓着它。
- Timer 说:“我只有在执行
invalidate之后才会放开你”。 - 开发者说:“我把
invalidate写在 ViewController 的dealloc里了”。 - 系统说:“因为 Timer 不放手,ViewController 进不去
dealloc,所以invalidate永远不会执行”。
结果: RunLoop Timer ViewController 这一整条链条全部驻留在内存中。
💡 终极修复方案
要打破这个全景图里的所有环,你需要:
-
Block:内部使用
[weak self]。 -
Delegate:属性声明必须为
weak。 -
Timer:
- 使用 iOS 10+ 的 Block 版本。
- 或者在
viewWillDisappear:中手动调用[timer invalidate](不等dealloc)。 - 或者引入一个 NSProxy 作为中间人(断开 Timer 对 ViewController 的强引用)。