美文网首页
一个不用担心循环引用的Timer

一个不用担心循环引用的Timer

作者: 22点的夜生活 | 来源:发表于2018-09-13 20:05 被阅读0次

    为什么要封装一个Timer

    • 项目中经常用到, 并且一不留神就会造成循环引用
    • 项目需要展示定时器有效的运行时间

    为什么选择GCD Timer

    Timer

    • Timer其实就是CFRunLoopTimerRef, 他们之间是toll-free bridged;
    • 一个Timer注册到RunLoop后, RunLoop会为其重复的时间点注册好事件,例如01:00、01:10这几个时间点;RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer;
    • Timer有个属性叫做Tolerance(宽容度),表示了当前时间点到后,允许有多少误差;
    • 由于Timer的这种机制,因此Timer的执行必须依赖于RunLoop,如果没有RunLoop则Timer不会执行, 如果RunLoop任务过于繁重, 可能就会导致Timer不准时;
    • 若加入RunLoop时设置的不是commonModes这个集合,也会受到影响;

    CADisplayLink

    • CADisplayLink是一个执行频率(fps)和屏幕刷新相同(可以修改preferredFramesPerSeconf改变刷新频率)的定时器,它也需要加入RunLoop才能执行;
    • 与NSTimer类似, CADisplayLink同样基于CFRunLoopTimerRef实现, 底层使用mk_timer;
    • 与Timer相比它的精度更高,不过和Timer类似的是如果遇到大任务,仍然存在丢帧现象; 通常情况下CADisplayLink用于构建帧动画,看起来更加流畅;

    GCD Timer

    • GCD则不同, GCD的线程管理是通过系统直接管理的, GCD Timer是通过dispatch port给RunLoop发送消息,来使RunLoop执行相应的block, 如果所在线程没有RunLoop, 那么GCD会临时创建一个线程去执行block,执行完之后销毁,因此GCD的Timer是不依赖RunLoop的;
    • 由于GCD Timer是通过port发送消息的机制来触发RunLoop的,如果RunLoop阻塞了, 还是会存在延迟的;

    代码

    执行方法
       /**
         * startTime: 开始时间, 默认立即开始
         * interval: 间隔时间, 默认1s
         * isRepeats: 是否重复执行, 默认true
         * isAsync: 是否异步, 默认false
         * task: 执行任务
         */
    class func execTask(startTime: TimeInterval = 0, interval: TimeInterval = 1, isRepeats: Bool = true, isAsync: Bool = false, task: @escaping ((_ duration: Int) -> Void)) -> String? {
            if (interval <= 0 && isRepeats) || startTime < 0 {
                return nil
            }
    
            let queue = isAsync ? DispatchQueue(label: "GCDTimer") : DispatchQueue.main
            let timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
            timer.schedule(deadline: .now() + startTime, repeating: 1.0, leeway: .milliseconds(0))
    
            semphore.wait()
            let name = "\(GCDTimer.timers.count)"
            timers[name] = timer
            timersState[name] = GCDTimerState.running
            durations[name] = 0
            fireTimes[name] = Date().timeIntervalSince1970
            semphore.signal()
    
            timer.setEventHandler {
                var lastTotalTime = durations[name] ?? 0
                let fireTime = fireTimes[name] ?? 0
                lastTotalTime = lastTotalTime + Date().timeIntervalSince1970 - fireTime
                task(lround(lastTotalTime))
                if !isRepeats {
                    self.cancelTask(task: name)
                }
            }
            timer.activate()
            return name
        }
    
    

    执行方法会返回一个任务字符串, 用于外界直接取消、暂停等操作

    // 使用默认值
    task1 = GCDTimer.execTask(task: { (totalTimer) in
              print("定时器运行有效时间(暂停时间不会计入): \(totalTimer)")
     })
    
    task2 = GCDTimer.execTask(startTime: 1, interval: 2, isRepeats: true, isAsync: false) { (_ ) in
              print("1s后开始, 定时器间隔2s, 允许重复执行, 不开启子线程")
     }
    
    取消定时器
    class func cancelTask(task: String?) {
            guard let _task = task else {
                return
            }
            semphore.wait()
            if timersState[_task] == .suspend {
                resumeTask(task: _task)
            }
            getTimer(task: _task)?.cancel()
    
            if let state = timersState.removeValue(forKey: _task) {
                print("The value \(state) was removed.")
            }
    
            if let timer = timers.removeValue(forKey: _task) {
                print("The value \(timer) was removed.")
            }
    
            if let fireTime = fireTimes.removeValue(forKey: _task) {
                print("The value \(fireTime) was removed.")
            }
    
            if let duration = durations.removeValue(forKey: _task) {
                print("The value \(duration) was removed.")
            }
    
            semphore.signal()
        }
    

    将开启定时器时反的task1/task2传入即可

    GCDTimer.cancelTask(task: task1)
    
    暂停
    class func suspendTask(task: String?) {
            guard let _task = task else {
                return
            }
    
            if timersState.keys.contains(_task) {
                timersState[_task] = .suspend
                getTimer(task: _task)?.suspend()
    
                var lastTotalTime = durations[_task] ?? 0
                let fireTime = fireTimes[_task] ?? 0
                lastTotalTime = lastTotalTime + Date().timeIntervalSince1970 - fireTime
                durations[_task] = lastTotalTime
            }
        }
    

    调用方式同取消定时器

    恢复定时器
    class func resumeTask(task: String?) {
            guard let _task = task else {
                return
            }
    
            if timersState.keys.contains(_task) && timersState[_task] != .running {
                fireTimes[_task] = Date().timeIntervalSince1970
                getTimer(task: task)?.resume()
                timersState[_task] = .running
            }
        }
    

    GCD Timer的resume与suspend是成对出现的, 所以不能重复resume

    GitHub地址

    https://github.com/zhangyadong1122/GCDTimer

    相关文章

      网友评论

          本文标题:一个不用担心循环引用的Timer

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