iOS Timers
整理一些老生常谈的问题.
timer即在经过一定的时间间隔后触发,向目标对象发送指定的消息.
iOS有三种timer机制:
Timer
DispatchSourceTimer
CADisplayLink
一:Timer
Timer基于runloop工作
scheduledTimer系列方法会将timer设定current runloop的defaultmode下
defaultmode自然不包含UITracking,当current runloop出在非default模式时,比如滑动UITableView,default的timer就不会触发.
init方法需要自己设置一个mode,如果选择common这个伪模式,就可以在default和tracking下同时生效
RunLoop.current.add(timer, forMode: .common)
偏差
Timer并不是一种实时机制,它取决于runloop的模式和状态,如果runloop处在不会检查timer的模式下,或者一些情况下没触发timer,那么触发的实际时间可能会延迟很多,这种机制叫做计时器偏差.
重复timer在相同的runloop中触发并重新调度自己,总是基于计划的时间间隔对自身进行调度,即便初始的调度时机已经存在偏差.
例如,如果一个timer被安排在某个特定的时间a调度,并且在此之后每隔5秒调度一次,实际的调度发生在a+n,那么之后的调度就是在a+n的基础上每隔5秒调度一次.
另外如果调度时间延迟到超过一个或多个计划时间,则计时器在该时间段内只触发一次,就是说不会去补偿,或者累积,而是忽略.
可以设定timer允许的偏差,runloop会在偏差范围内进行调度,据apple的文档所说,这有利于系统优化,节省电量.
默认是0,但是不代表不存在偏差,就像前面说的,本身就存在偏差.
apple建议设置为时间间隔的10%.
其他特性
重复timer需要主动执行invalidate()来销毁,并且invalidate和在timer的创建必须在同一个线程.
当调用invalidate()时,runloop也不一定会立即释放timer,可能存在延迟.
Timer不仅可以使用block或者target+selector初始化,甚至可以直接用NSInvocation初始化,可以直接包装一个NSInvocation对象,指定target,selector,参数和返回值.
不过swift不能用NSInvocation
fire()方法可以立即调度timer,并且不影响重复timer的调度周期,不过非重复timer在fire()之后立即销毁,相当于提前执行,销毁之后则不能fire.
fireDate属性很独特,支持set get,能够获取和调整timer下一次调度的时间,相较于反复的创建和销毁,调整fireDate更加合适,可以通过fireDate设置一个极大的值,来实现暂停timer.
timer.fireDate = .distantFuture
关于循环强引用
runloop会维护对计时器的强引用,因此在将计时器添加到运行循环后,不必维护自己对计时器的强引用.
非重复timer触发一次,然后自动使自身失效,runloop随即释放它,无需其他操作.
对于会重复的timer,需要防止timer和target的循环强引用.
常用的解决方案:
1.使用block版本,弱化target
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] t in
self?.event()
})
2.使用中间对象转发
A创建并持有timer,timer使用中间对象B作为target,A持有B的weak版.
当B接收到消息时,调用forwardingTarget转发给A.
//class B
class TimerTarget : NSObject{
weak var target : NSObject?
init(t:NSObject){
target = t
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
}
//class A
timer = Timer.scheduledTimer(timeInterval: 1.0, target: TimerTarget.init(t: self), selector: #selector(event), userInfo: nil, repeats: true)
deinit{
timer?.invalidate()
}
3.使用NSProxy来转发
和上面的基本一样,但是NSProsy的子类要重写- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel 和 - (void)forwardInvocation:(NSInvocation *)invocation,swift不行,需要创建OC文件.
#import "TimerProxy.h"
@interface TimerProxy ()
@property (nonatomic, weak) id target;
@end
@implementation TimerProxy
- (instancetype)initWithTarget:(id)target{
self = [TimerProxy alloc];
self.target = target;
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
timer = Timer.scheduledTimer(timeInterval: 1.0, target: TimerProxy.init(target: self), selector: #selector(event), userInfo: nil, repeats: true)
deinit{
timer?.invalidate()
}
二:DispatchSourceTimer
protocol DispatchSourceTimer是一个协议,不过不需要实现这个协议,而是通过DispatchSource的makeTimerSource(flags:queue:)方法初始化一个对象,这个对象遵循DispatchSourceTimer协议.
var timer : DispatchSourceTimer?
timer = DispatchSource.makeTimerSource(flags: .strict, queue: .main)
timer?.schedule(deadline: .now(), repeating: 1.0)
timer?.setEventHandler(handler: { [weak self] in
self?.event()
})
flags是DispatchSource.TimerFlags,一般使用.strict,此时系统会尽最大可能满足timer的准确性,也可以不设置
timer = DispatchSource.makeTimerSource(queue: .main)
DispatchSourceTimer仍然存在可能的延迟,并且可以设置容忍的范围,默认值为0,系统会尽可能的满足准确性
public func schedule(deadline: DispatchTime, repeating interval: Double, leeway: DispatchTimeInterval = .nanoseconds(0))
除了设置任务,还可以设置激活和销毁时的回调
timer?.setRegistrationHandler(handler: {
print("active")
})
timer?.setCancelHandler(handler: {
print("cancel")
})
DispatchSourceTimer一共有四种动作
timer?.activate() //开始
timer?.suspend() //暂停
timer?.resume() //继续
timer?.cancel() //销毁
0.DispatchSourceTimer与runloop无关,也不由系统强引用,需要自己维护强引用,局部变量一旦离开作用域就会释放
1.activate和resume都可以作为开始
2.activate和resume不能连续出现,正在活跃的timer不能继续给活跃的指令,会crash
3.suspend不会立即暂停,而是会在本次执行完暂停
4.suspend状态时,如果释放timer会引起crash
5.suspend状态时调用cancel是无意义的,不会调用cancelhandle,释放timer仍然会引起carsh
6.cancel之后虽然timer对象还在,但是无法再激活,需要重新获取.
7.suspend和cancel指令是会累加次数的,几次暂停就需要几次恢复.
//这样释放会crash
suspend();
suspend();
resume();
//这样会继续运行,并且释放不会crash
suspend();
suspend();
resume();
resume();
DispatchSourceTimer无法获取当前的状态,不能在使用动作之前先做检查,需要谨慎安排动作.
也可以不使用suspend,用cancel来暂停,需要继续就重新创建,再配合高精度repeating来避免误差.
把repeating设置为.never,就会只执行一次.
cancel()可以立即销毁dispatchSourceTimer,即将要执行的任务也不会执行.
结合这两点,设置deadline来控制延时,就达成了可随时取消执行的延时操作,相较于Dispatchafter更方便.
func start(){
timer = DispatchSource.makeTimerSource(flags: .strict, queue: .main)
timer?.schedule(deadline: .now() + 2, repeating: .never)
timer?.setEventHandler {
print("\(Date.init().timeIntervalSince1970)")
}
timer?.resume()
}
//取消
func end(){
timer?.cancel()
}
Timer需要考虑runloop的问题,而DispatchSourceTimer则需要考虑线程的问题,虽然timer是异步的,但是需要注意主队列不会开启新线程,主队列有耗时操作时,timer会因为等待前面的任务而产生延迟.
DispatchSourceTimer的eventHandle可以暂停,但是deadline不能暂停,在deadline期间调用resume,deadline会继续消耗,当消耗完的时候,什么也不会做,当等到resume的时候,立即开始触发event.
也就是说暂不暂停不影响deadline的消耗.
三:后台计时
不管是哪种timer,都不能在后台被触发,一旦进入后台就会暂停,回到前台会恢复.
个人理解,timer这种简单的机制不应该考虑后台保活,而是应该通过恢复现场来完善,应该通过进入后台和回到前台的时间差来模拟真实的计时.
现在来实现一个能够保存后台到前台时间差的DispatchSourceTimer,顺便把初始化需要的三个方法改成一个常用方法.
首先DispatchSourceTimer是个协议,而且还不能遵循它,只能通过DispatchSource.makeTimerSource来获取一个遵循协议的对象.
其次DispatchSource的makeTimerSource方法不能重写,因此协议,继承都不合适,只能用新的类包装了.
class BackgroundGCDTimer{
var timer:DispatchSourceTimer
var timeInterval = NSDate.now.timeIntervalSince1970
var foregroundHandler:((TimeInterval)->())?
}
一个timer,一个记录时间戳,一个回到前台时的回调.
init(flags:DispatchSource.TimerFlags = .strict, queue:DispatchQueue = .main, active:Bool = true, deadline: DispatchTime, repeating: Double, handler: DispatchSourceProtocol.DispatchSourceHandler?, foregroundHandler foreground:((TimeInterval)->())? = nil){
timer = DispatchSource.makeTimerSource(flags: flags, queue: queue)
timer.schedule(deadline: deadline, repeating: repeating)
timer.setEventHandler(handler: handler)
if active{
timer.activate()
}
foregroundHandler = foreground
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
初始化方法可以把配置项都写进去,并且提供默认值
但是这还没有考虑deadline期间进入后台的情况
在进入后台时,deadline仍然是有效的,当从后台回来时,如果deadline没走完,那就会继续走,这个时间是准确的;
如果在后台期间deadline就走完了,那么也不会触发eventhandle,当回到前台时,才会触发.
我猜想机制和这个例子类似,也是计算时差,实际上后台什么都没干,回来的时候减去时差看看deadline走没走完,走完了就可以开始触发event了;
所以对deadline计时也是timer的工作内容.
所以deadline期间进入后台,不更新时间戳,回到前台时,也不需要算时间差.
但是也获取不到deadline的状态,所以我选择在第一次执行event的时候进行标记
不过这个timer在后台deadline走完时也不会立即执行event,和DispatchSourceTimer是一样的,或者说deadline走完也不会计算时差,
如果我们希望deadline和真实的计时无缝衔接,就需要自己实现deadline了
1.定义一个nowTime,它是计时器计过的时间
2.timer每0.001秒执行一次(存在精度问题,需要更详细的设计),每次执行增加nowTime += 0.001
3.进入后台就更新时间戳
4.回到前台计算nowTime + 时间差 - deadline 是不是大于0,大于0则说明真实的计时已经开始.并且需要更新nowTime
5.当nowTime大于deadline并且是repeating的整数倍时执行event
6.再加一个convenience方法: 当回到前台时立即循环执行(时差/repeating)次handler,相当于把少的次数补回来.max表示预估最大次数.
7.管理状态,并且限制不同状态下的行为
8.添加OC接口
let accuracy = 0.001
let multiple = 1.0 / accuracy
enum GLDispatchTimerState{
case inactive
case working
case suspend
case cancelled
}
@objcMembers
class GLDispatchTimer:NSObject{
var timer:DispatchSourceTimer?
var timeInterval = NSDate.init().timeIntervalSince1970
var foregroundHandler:((Double)->())?
var state:GLDispatchTimerState = .inactive
var nowTime:Double = 0
var deadline:Double = 0
static func create(deadlineTime: Double, repeating: Double, handler: DispatchSourceProtocol.DispatchSourceHandler?, foregroundHandler foreground:((Double)->())?) -> GLDispatchTimer{
return GLDispatchTimer.init(flags: .strict, queue: .main, active: true, deadlineTime: deadlineTime, repeating: repeating, handler: handler, foregroundHandler: foreground)
}
static func create(deadlineTime: Double, repeating: Double, max:Int, commonHandler: (DispatchSourceProtocol.DispatchSourceHandler?)) -> GLDispatchTimer{
return GLDispatchTimer.init(flags: .strict, queue: .main, active: true, deadlineTime: deadlineTime, repeating: repeating, handler: commonHandler) { count in
for _ in 0 ..< min(Int(count * multiple) / Int(repeating * multiple), max){
commonHandler?()
}
}
}
convenience init(flags:DispatchSource.TimerFlags = .strict, queue:DispatchQueue = .main, active:Bool = true, deadlineTime: Double, repeating: Double, max:Int, commonHandler: DispatchSourceProtocol.DispatchSourceHandler?) {
self.init(flags: flags, queue: queue, active: active, deadlineTime: deadlineTime, repeating: repeating, handler: commonHandler) { count in
for _ in 0 ..< min(Int(count * multiple) / Int(repeating * multiple), max){
commonHandler?()
}
}
}
init(flags:DispatchSource.TimerFlags = .strict, queue:DispatchQueue = .main, active:Bool = true, deadlineTime: Double, repeating: Double, handler: DispatchSourceProtocol.DispatchSourceHandler?, foregroundHandler foreground:((Double)->())? = nil){
timer = DispatchSource.makeTimerSource(flags: flags, queue: queue)
foregroundHandler = foreground
deadline = deadlineTime
super.init()
timer?.schedule(deadline: .now(), repeating: accuracy)
timer?.setEventHandler {[weak self] in
guard let self = self else{return}
self.nowTime += accuracy
if self.nowTime >= self.deadline && Int(self.nowTime * multiple) % Int(repeating * multiple) == 0{
handler?()
}
}
if active{
activate()
}
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
func activate(){
guard state == .inactive else{return}
timer?.activate()
state = .working
}
func cancel(){
if state == .suspend{
timer?.resume()
}
timer?.cancel()
state = .cancelled
}
func suspend(){
guard state == .working else{return}
state = .suspend
timer?.suspend()
}
func resume(){
guard state != .working && state != .cancelled else{return}
timer?.resume()
state = .working
}
@objc func didEnterBackground(){
suspend()
timeInterval = Date.init().timeIntervalSince1970
}
@objc func willEnterForeground(){
let dvalue = Date.init().timeIntervalSince1970 - timeInterval
let sec = nowTime + dvalue - deadline
if sec > 0{
foregroundHandler?(dvalue)
}
nowTime += dvalue
resume()
}
deinit{
NotificationCenter.default.removeObserver(self)
}
}