7-10.【OC】【RunLoop】NSTimer 的正确创建方式和销毁姿势是什么?

5 阅读2分钟

要正确使用 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

  1. 停止计时:停止内部的计时逻辑。
  2. 移出 RunLoop:将自己从 RunLoop 的监听队列中移除(RunLoop 会释放对 Timer 的强引用)。
  3. 释放 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变量指针清空,彻底回收。