NSTimer

Timer 表示一个定时器,在经过一定的时间间隔后触发,向目标对象发送指定的消息。

1
class Timer : NSObject
  • Timer与run loop一起工作,run loop 维护对其Timer的强引用,因此在将Timer添加到run loop后,你不必维护自己对Timer的强引用。

  • Timer 不是实时机制。如果Timer的触发时间发生在长时间run loop调用期间 或者 run loop处于不监视timer的 mode 时,则直到run loop下次检查timer时,Timer才会启动。因此,Timer触发的实际时间可能要晚得多。

  • Timer 和 Core Foundation 的CFRunLoopTimer 是桥接的。

  • 不要子类化Timer

可以指定timer在创建时是repeating 还是 nonrepeating。一个nonrepeating timer触发一次,然后自动失效,从而防止计时器再次触发。相比之下,一个repeating timer触发,然后在同一个run loop中重新安排自己。repeating timer总是根据 计划的触发时间 而不是 实际的触发时间 来调度自己。例如,如果一个timer计划在某个特定时间触发,并且在该时间之后每隔5秒触发一次,则即使实际触发时间被延迟,计划的触发时间也将始终落在原来的5秒时间间隔上。如果触发时间延迟得太远,以至于超过了计划的触发时间中的一个或多个,则在该时间段内仅触发一次timer;否则,计时器将被触发一次。 计时器会在触发后重新安排为将来的下一个计划的触发时间。

创建 Timer 有如下三种方式:

方法一

使用此类方法创建 timer,并在 default Mode 下在当前run loop上调度 timer。

1
2
3
4
5
6
7
8
9
class func scheduledTimer(timeInterval ti: TimeInterval, 
invocation: NSInvocation,
repeats yesOrNo: Bool) -> Timer

class func scheduledTimer(timeInterval ti: TimeInterval,
target aTarget: Any,
selector aSelector: Selector,
userInfo: Any?,
repeats yesOrNo: Bool) -> Timer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
weak var timer: Timer?

timer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(timerAction(_:)), userInfo: "间隔3秒执行一次", repeats: true)
// timer?.fire(),表示让timer立即执行,如果不执行此方法,会等待指定的interval后再首次执行
// timer?.fireDate = Date(),表示timer首次触发的事件。

@objc private func timerAction(_ timer: Timer) {
guard let info = timer.userInfo else { return }
print(info)
}

// 移除timer
private func endAction() {
if timer?.isValid == true { timer?.invalidate() }
}
1
2
3
4
5
if #available(iOS 10.0, *) {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
print("每隔1秒执行")
}
}

方法二

使用此类方法创建 timer,而不会在run loop上调度它。创建计时器后,必须通过调用相应RunLoop 对象的 add(_:forMode:)方法将 timer 手动添加到run loop中。

1
2
3
init(timeInterval ti: TimeInterval, invocation: NSInvocation, repeats yesOrNo: Bool)

init(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
weak var reTimer: Timer?

let timer = Timer(timeInterval: 1, target: self, selector: #selector(timerAction(_:)), userInfo: "每隔1秒执行一次", repeats: true)
RunLoop.current.add(timer, forMode: .common)
reTimer = timer

@objc private func timerAction(_ timer: Timer) {
guard let info = timer.userInfo else { return }
print(info)
}

// 移除timer
private func endAction() {
if reTimer?.isValid == true { reTimer?.invalidate() }
}
1
2
3
4
5
6
if #available(iOS 10.0, *) {
let timer = Timer(timeInterval: 1, repeats: true) { (_) in
print("每隔1秒")
}
RunLoop.current.add(timer, forMode: .common)
}

方法三

使用初始化方法配置 timer。创建完成后,必须通过调用相应 RunLoop 对象的 add(_:forMode:) 方法将timer手动添加到 run loop 中。

1
init(fireAt date: Date, interval ti: TimeInterval, target t: Any, selector s: Selector, userInfo ui: Any?, repeats rep: Bool)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
weak var reTimer: Timer?

let timer = Timer(fireAt: Date(), interval: 3.0, target: self, selector: #selector(timerAction(_:)), userInfo: "每隔1秒", repeats: true)
RunLoop.current.add(timer, forMode: .common)
reTimer = timer

@objc private func timerAction(_ timer: Timer) {
guard let info = timer.userInfo else { return }
print(info)
}

// 移除timer
private func endAction() {
if reTimer?.isValid == true { reTimer?.invalidate() }
}
1
2
3
4
5
6
if #available(iOS 10.0, *) {
let timer = Timer(fire: Date(timeIntervalSinceNow: 1.0), interval: 3.0, repeats: true) { (timer) in
print("此方法可以指定timer最开始的触发事件")
}
RunLoop.current.add(timer, forMode: .common)
}

注意:
方法一和方法二,默认情况下 timer 创建完成后,不会立即触发,而是等待 interval 后再首次触发。当然,你可以通过 fire() 或 fireDate 设置其首次触发的事件。

另外,timer 会保留 target 的强引用,直到它invalidated。

一旦在run loop上进行scheduled(调度),timer将按指定的时间间隔fires(触发),直到它invalidated(无效)为止。nonrepeating timer在 fires 后立即失效;对于 repeating timer,必须调用timer的invalidate()方法使其无效。

invalidate() 方法会把 timer 从当前 run loop中删除,因此,你应该始终从安装 timer 的同一线程中调用invalidate()方法。

使timer无效会使它立即禁用,以使其不再影响run loop。然后,run loop将在invalidate()方法返回之前或稍后某个时间删除timer(以及它对timer的强引用)。timer一旦invalidated,timer对象将无法重用。

repeating timer触发后,它会在指定的tolerance(容差)范围内,为最近的 将来日期 安排下一次触发,该日期是上次计划的触发日期之后的时间间隔的整数倍。

如果调出 执行Selector或invocation 所花费的时间长于指定的间隔,则timer仅安排下一次触发;也就是说,timer 不会尝试补偿在调用指定的selector或invocation时可能发生的任何丢失的触发。

Timer 的内存泄漏

对于 repeating timer 在 iOS10.0 之前类似 target: self, selector:的API ,由于 repeating timer 被 run loop 强引用,而且 timer 会对 target 强引用。如果开发者未在合适的时机调用 timer 的 invalidate() 方法,则该 target 不会被释放,selector 不会停止执行。

另外,若 target 保留对 timer 的强引用,则会会造成 循环引用。

苹果在 iOS 10.0 之后,给出了使用 闭包 的方式可以避免如上情况的发生,那么在 iOS 10.0 以前,我们可以参考 BlocksKit/BlocksKit 给出的方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation NSTimer (BlocksKit)

+ (instancetype)bk_scheduleTimerWithTimeInterval:(NSTimeInterval)seconds repeats:(BOOL)repeats usingBlock:(void (^)(NSTimer *timer))block
{
NSTimer *timer = [self bk_timerWithTimeInterval:seconds repeats:repeats usingBlock:block];
[NSRunLoop.currentRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
return timer;
}

+ (instancetype)bk_timerWithTimeInterval:(NSTimeInterval)inSeconds repeats:(BOOL)repeats usingBlock:(void (^)(NSTimer *timer))block
{
NSParameterAssert(block != nil);
CFAbsoluteTime seconds = fmax(inSeconds, 0.0001);
CFAbsoluteTime interval = repeats ? seconds : 0;
CFAbsoluteTime fireDate = CFAbsoluteTimeGetCurrent() + seconds;
return (__bridge_transfer NSTimer *)CFRunLoopTimerCreateWithHandler(NULL, fireDate, interval, 0, 0, (void(^)(CFRunLoopTimerRef))block);
}

@end

对于 swift 的计时器可以学习 SwiftTimer,该库是根据 DispatchSourceTimer 实现的。

学习博客:

Timer Sources

High Precision Timers in iOS / OS X

Effective Objective-C 2.0 笔记

Crash 防护方案(四):NSTimer

文章作者: Czm
文章链接: http://yoursite.com/2020/04/26/NSTimer/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm!