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 类型的对象。NSTimer 在 init 时,内部会执行类似 _target = [target retain]; 的操作。它并不关心你传入的是不是一个弱引用指针,它只看指针指向的那个内存地址,并对该地址的对象进行强引用。
3. 为什么 dealloc 不会执行?
由于 RunLoop Timer ViewController 这条强引用链的存在:
-
你想在
dealloc里调用[timer invalidate]。 -
但因为
timer强引用了ViewController,导致ViewController的引用计数永远不为 0。 -
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 状态,这条强引用链就一直存在。