7-13.【OC】【RunLoop】Timer / DisplayLink 的销毁时机为什么重要?

5 阅读3分钟

TimerCADisplayLink 的销毁时机之所以至关重要,是因为它们的设计深度耦合了 内存管理(ARC)线程生命周期 以及 CPU/GPU 资源消耗

如果销毁时机不当,轻则导致内存泄漏,重则引发 App 掉帧、发热甚至崩溃。以下是核心原因的深度解析:


1. 彻底打破“内存死结”

这是最直接的原因。正如我们之前讨论的,TimerCADisplayLink 都会强引用它们的 target

  • 死结逻辑ViewController 强引用 Timer,而 Timer 强引用 ViewController
  • 销毁时机的重要性:如果你计划在 dealloc 中销毁 Timer,你将永远等不到那一天。因为 Timer 不停,ViewController 的引用计数就永远不为 0。
  • 正确做法:必须在一个早于 dealloc 的明确时机(如 viewWillDisappear 或业务逻辑结束点)调用 invalidate

2. 释放 RunLoop 的持有权

Timer 并不是一个孤立存在的对象,它必须“寄生”在 RunLoop 中才能工作。

  • 持有关系:当你把 Timer 加入 RunLoop 后,RunLoop 会强引用 Timer
  • 风险:如果你只是将 timer = nil 而没有调用 invalidateTimer 其实依然活在 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用户离开页面,任务即刻停止,防止后台空跑。
动画 / 游戏循环didEnterBackgroundApp 进入后台时应停止所有 UI 刷新逻辑。
常驻后台任务业务逻辑 Completion 闭包任务完成后立即自杀,防止内存堆积。
单例模式AppTerminate随 App 生命周期结束,但通常建议按需开启。