Timer 和 CADisplayLink 的销毁时机之所以至关重要,是因为它们的设计深度耦合了 内存管理(ARC) 、线程生命周期 以及 CPU/GPU 资源消耗。
如果销毁时机不当,轻则导致内存泄漏,重则引发 App 掉帧、发热甚至崩溃。以下是核心原因的深度解析:
1. 彻底打破“内存死结”
这是最直接的原因。正如我们之前讨论的,Timer 和 CADisplayLink 都会强引用它们的 target。
- 死结逻辑:
ViewController强引用Timer,而Timer强引用ViewController。 - 销毁时机的重要性:如果你计划在
dealloc中销毁Timer,你将永远等不到那一天。因为Timer不停,ViewController的引用计数就永远不为 0。 - 正确做法:必须在一个早于
dealloc的明确时机(如viewWillDisappear或业务逻辑结束点)调用invalidate。
2. 释放 RunLoop 的持有权
Timer 并不是一个孤立存在的对象,它必须“寄生”在 RunLoop 中才能工作。
- 持有关系:当你把
Timer加入RunLoop后,RunLoop会强引用Timer。 - 风险:如果你只是将
timer = nil而没有调用invalidate,Timer其实依然活在RunLoop的注册表里。它会继续在后台尝试触发,并继续强引用着你的ViewController。 - 重要性:只有显式销毁,才能让
RunLoop彻底释放该任务,保证内存回收。
3. 防止“僵尸回调”导致崩溃
如果 Timer 的销毁时机晚于其相关数据的清理时机,就会发生野指针访问。
- 场景:子线程的
Timer仍在运行,但它依赖的某些数据模型已经在主线程被释放了。 - 后果:当
Timer下一次触发时,它尝试访问已经释放的内存,直接导致EXC_BAD_ACCESS崩溃。 - 重要性:销毁时机必须在数据模型失效之前。
4. 节省 CPU 和电池寿命(尤其是 CADisplayLink)
CADisplayLink 的工作频率通常是 60Hz 或 120Hz,每一秒钟都要执行 60-120 次回调。
- 资源浪费:如果一个界面已经退出了,但
CADisplayLink还在后台疯狂空转,它会强制 CPU 和 GPU 保持活跃状态,阻碍设备进入低功耗模式。 - 后果:用户会感觉到手机发热严重、耗电飞快。
- 重要性:一旦 UI 不可见,立即销毁
DisplayLink是移动端性能优化的基本准则。
5. 线程安全与生命周期对齐
Timer 的创建和销毁必须在同一个线程。
- 潜在问题:如果你在主线程创建了
Timer,却在后台线程执行销毁,可能会导致RunLoop的内部注册表状态不一致。 - 重要性:掌握正确的销毁时机,能确保线程间的状态机同步,避免多线程竞态条件。
总结:销毁时机的最佳实践对照表
| 场景 | 推荐销毁位置 | 理由 |
|---|---|---|
| 页面倒计时 | viewWillDisappear | 用户离开页面,任务即刻停止,防止后台空跑。 |
| 动画 / 游戏循环 | didEnterBackground | App 进入后台时应停止所有 UI 刷新逻辑。 |
| 常驻后台任务 | 业务逻辑 Completion 闭包 | 任务完成后立即自杀,防止内存堆积。 |
| 单例模式 | AppTerminate | 随 App 生命周期结束,但通常建议按需开启。 |