要正确使用 NSTimer(或 Swift 中的 Timer),必须平衡好 RunLoop 模式、内存管理和线程安全这三个维度。
1. 正确的创建姿势
根据应用场景的不同,创建方式分为两类:
A. 自动注册到当前 RunLoop(最常用)
这种方式最方便,但要注意它默认只运行在 NSDefaultRunLoopMode 下。
Objective-C
// Objective-C
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(tick)
userInfo:nil
repeats:YES];
// Swift (iOS 10+)
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
B. 手动控制 RunLoop 和 Mode(进阶)
如果你希望在列表滑动(UITrackingRunLoopMode)时定时器不停止,必须使用这种方式:
Objective-C
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(tick)
userInfo:nil
repeats:YES];
// 手动加入 CommonModes,确保滑动时不被挂起
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
2. 正确的销毁姿势(最关键)
NSTimer 的销毁不仅仅是将变量设为 nil,它必须经历 “失效 -> 移除 -> 置空” 三部曲。
核心操作:invalidate
- 停止计时:停止内部的计时逻辑。
- 移出 RunLoop:将自己从 RunLoop 的监听队列中移除(RunLoop 会释放对 Timer 的强引用)。
- 释放 Target:Timer 会释放它对
target(通常是 ViewController)的强引用。
销毁的时机:不要在 dealloc 中销毁!
由于 repeats: YES 的 Timer 会强引用 self,只要 Timer 不停,dealloc 永远不会执行。
- 推荐方案:在
viewWillDisappear:、viewDidDisappear:或明确的业务结束点(如点击了“停止”按钮)进行销毁。
Objective-C
- (void)clearTimer {
if (self.timer) {
[self.timer invalidate]; // 必须先调用这个!
self.timer = nil; // 然后才安全地置空
}
}
3. 三大防坑守则
第一准则:处理重复触发的循环引用
如果你使用的是 iOS 10 以下的老 API,或者必须使用 target: self 模式,请务必使用 Proxy(代理对象) 或 Block 包装器 来打破强引用循环。
第二准则:线程安全
NSTimer 不是线程安全的。
- 创建与销毁必须在同一个线程:如果你在主线程创建了 Timer,却尝试在后台线程调用
invalidate,可能会引发内存泄漏或不可预知的崩溃。
第三准则:容差(Tolerance)设置
为了节省电力(能耗优化),建议设置 tolerance。这允许系统在不影响用户感知的情况下,微调 Timer 的触发时间,以便合并多个任务共同唤醒 CPU。
Objective-C
self.timer.tolerance = 0.1; // 允许有 10% 的误差
总结:Timer 生命周期流程图
| 步骤 | 执行动作 | 内存/引用变化 |
|---|---|---|
| 1. 创建 | scheduledTimer... | RunLoop 强引用 Timer,Timer 强引用 Target。 |
| 2. 运行 | RunLoop 循环回调 | 正常执行业务代码。 |
| 3. 停止 | [timer invalidate] | RunLoop 释放 Timer,Timer 释放 Target。 |
| 4. 置空 | timer = nil | 变量指针清空,彻底回收。 |