美文网首页
iOS Timers

iOS Timers

作者: Trigger_o | 来源:发表于2022-06-29 11:18 被阅读0次

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

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

    相关文章

      网友评论

          本文标题:iOS Timers

          本文链接:https://www.haomeiwen.com/subject/fehuvrtx.html