iOS Timers

2022-06-29  本文已影响0人  Trigger_o

整理一些老生常谈的问题.

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)
    }

}


上一篇下一篇

猜你喜欢

热点阅读