美文网首页
一个不用担心循环引用的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