NSTimer

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

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

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

  • TimerCore FoundationCFRunLoopTimer 是桥接的。

  • 不要子类化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 timerfires 后立即失效;对于 repeating timer,必须调用timerinvalidate()方法使其无效。

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

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

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

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

Timer 的内存泄漏

对于 repeating timer 在 iOS10.0 之前类似 target: self, selector:的API ,由于 repeating timerrun loop 强引用,而且 timer 会对 target 强引用。如果开发者未在合适的时机调用 timerinvalidate() 方法,则该 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