7-9.【OC】【RunLoop】NSTimer 为什么容易产生 retain cycle?

1 阅读2分钟

NSTimer 产生循环引用(Retain Cycle)的根本原因在于其 设计模式内存管理机制 之间的冲突。


1. 核心矛盾:谁持有谁?

在常规的委托(Delegate)模式中,通常是 weak 引用。但在 NSTimer 的设计中,情况截然不同:

  • 开发者持有 Timer:为了在合适的时机停止定时器,我们通常会在 ViewController 中用一个 strong 属性持有 timer
  • RunLoop 持有 Timer:当你调用 scheduledTimer... 时,定时器被添加到了 NSRunLoop 中。RunLoop 会强引用这个 Timer
  • Timer 持有 Target:这是最关键的一点。NSTimer 的内部实现会对传入的 target 对象进行 强引用(为了保证在触发 Action 时对象依然存活)。

2. 为什么 weakSelf 无效?

这是很多开发者最容易踩的坑。在闭包(Block)中,我们可以通过 [weak self] 解决循环引用。但在老版本的 NSTimer API 中:

Objective-C

// 即使 self 是 weak,NSTimer 内部依然会把这个对象重新强引用一次
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
                                              target:self 
                                            selector:@selector(doSomething) 
                                            userInfo:nil 
                                             repeats:YES];

原因分析:

target 参数接收的是一个 id 类型的对象。NSTimerinit 时,内部会执行类似 _target = [target retain]; 的操作。它并不关心你传入的是不是一个弱引用指针,它只看指针指向的那个内存地址,并对该地址的对象进行强引用。


3. 为什么 dealloc 不会执行?

由于 RunLoop \rightarrow Timer \rightarrow ViewController 这条强引用链的存在:

  1. 你想在 dealloc 里调用 [timer invalidate]

  2. 但因为 timer 强引用了 ViewController,导致 ViewController 的引用计数永远不为 0。

  3. dealloc 永远不会被触发,timer 永远不会被销毁。

    结论:这是一个死结。


4. 解决方案:如何优雅地打破循环?

方案 A:使用 iOS 10+ 的 Block API

苹果在 iOS 10 之后引入了基于 Block 的 API,这允许我们使用 [weak self]

Swift

// Swift 示例
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.doSomething()
}

方案 B:使用“中介者” (Proxy)

创建一个轻量级的代理对象(通常继承自 NSProxy),让 Timer 强引用 Proxy,而 Proxy 弱引用 ViewController

方案 C:手动管理销毁时机

不要依赖 dealloc。在 viewWillDisappear: 或其他明确的生命周期终点手动执行:

Objective-C

[self.timer invalidate];
self.timer = nil;

总结

NSTimer 的循环引用不是因为闭包捕获,而是因为 RunLoop 的生命周期管理机制强行持有了 Target。只要定时器是 repeats: YES 状态,这条强引用链就一直存在。