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

为什么要封装一个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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容

  • 最近看了很多RunLoop的文章,看完很懵逼,决心整理一下,文章中大部分内容都是引用大神们的,但好歹对自己有个交代...
    小凉介阅读 6,708评论 12 79
  • 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多...
    阳明先生_X自主阅读 1,101评论 0 17
  • ios 常用的定时器有三种:NSTime,CADisplayLink和GCD。 NsTimer // 参数:Int...
    殿小七阅读 835评论 0 2
  • iOS刨根问底-深入理解RunLoop 2017-05-08 10:35 by KenshinCui 概述 Run...
    mengjz阅读 1,559评论 1 10
  • 神奇旅馆|001 萝卜和泊雅 这是一家叫做神奇的旅馆,在这里面住着许多形态各异的人,对于住在这里的人而言,找寻人生...
    懒猫吃鱼不吃饭阅读 522评论 0 2