一個不用擔心循環(huán)引用的Timer

為什么要封裝一個Timer

  • 項目中經(jīng)常用到, 并且一不留神就會造成循環(huán)引用
  • 項目需要展示定時器有效的運行時間

為什么選擇GCD Timer

Timer

  • Timer其實就是CFRunLoopTimerRef, 他們之間是toll-free bridged;
  • 一個Timer注冊到RunLoop后, RunLoop會為其重復(fù)的時間點注冊好事件,例如01:00仔拟、01:10這幾個時間點;RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調(diào)這個Timer;
  • Timer有個屬性叫做Tolerance(寬容度),表示了當前時間點到后,允許有多少誤差;
  • 由于Timer的這種機制,因此Timer的執(zhí)行必須依賴于RunLoop,如果沒有RunLoop則Timer不會執(zhí)行, 如果RunLoop任務(wù)過于繁重, 可能就會導致Timer不準時;
  • 若加入RunLoop時設(shè)置的不是commonModes這個集合,也會受到影響;

CADisplayLink

  • CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSeconf改變刷新頻率)的定時器,它也需要加入RunLoop才能執(zhí)行;
  • 與NSTimer類似, CADisplayLink同樣基于CFRunLoopTimerRef實現(xiàn), 底層使用mk_timer;
  • 與Timer相比它的精度更高,不過和Timer類似的是如果遇到大任務(wù),仍然存在丟幀現(xiàn)象; 通常情況下CADisplayLink用于構(gòu)建幀動畫,看起來更加流暢;

GCD Timer

  • GCD則不同, GCD的線程管理是通過系統(tǒng)直接管理的, GCD Timer是通過dispatch port給RunLoop發(fā)送消息,來使RunLoop執(zhí)行相應(yīng)的block, 如果所在線程沒有RunLoop, 那么GCD會臨時創(chuàng)建一個線程去執(zhí)行block,執(zhí)行完之后銷毀,因此GCD的Timer是不依賴RunLoop的;
  • 由于GCD Timer是通過port發(fā)送消息的機制來觸發(fā)RunLoop的,如果RunLoop阻塞了, 還是會存在延遲的;

代碼

執(zhí)行方法
   /**
     * startTime: 開始時間, 默認立即開始
     * interval: 間隔時間, 默認1s
     * isRepeats: 是否重復(fù)執(zhí)行, 默認true
     * isAsync: 是否異步, 默認false
     * task: 執(zhí)行任務(wù)
     */
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
    }

執(zhí)行方法會返回一個任務(wù)字符串, 用于外界直接取消、暫停等操作

// 使用默認值
task1 = GCDTimer.execTask(task: { (totalTimer) in
          print("定時器運行有效時間(暫停時間不會計入): \(totalTimer)")
 })

task2 = GCDTimer.execTask(startTime: 1, interval: 2, isRepeats: true, isAsync: false) { (_ ) in
          print("1s后開始, 定時器間隔2s, 允許重復(fù)執(zhí)行, 不開啟子線程")
 }
取消定時器
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
        }
    }

調(diào)用方式同取消定時器

恢復(fù)定時器
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是成對出現(xiàn)的, 所以不能重復(fù)resume

GitHub地址

https://github.com/zhangyadong1122/GCDTimer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市双炕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尘分,老刑警劉巖踏堡,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異误证,居然都是意外死亡,警方通過查閱死者的電腦和手機修壕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門愈捅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人慈鸠,你說我怎么就攤上這事蓝谨。” “怎么了青团?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵譬巫,是天一觀的道長。 經(jīng)常有香客問我督笆,道長芦昔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任娃肿,我火速辦了婚禮咕缎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘料扰。我一直安慰自己凭豪,他們只是感情好,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布晒杈。 她就那樣靜靜地躺著嫂伞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪桐智。 梳的紋絲不亂的頭發(fā)上末早,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音说庭,去河邊找鬼然磷。 笑死,一個胖子當著我的面吹牛刊驴,可吹牛的內(nèi)容都是我干的姿搜。 我是一名探鬼主播寡润,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舅柜!你這毒婦竟也來了梭纹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤致份,失蹤者是張志新(化名)和其女友劉穎变抽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氮块,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡绍载,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了滔蝉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片击儡。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蝠引,靈堂內(nèi)的尸體忽然破棺而出阳谍,到底是詐尸還是另有隱情,我是刑警寧澤螃概,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布矫夯,位于F島的核電站,受9級特大地震影響谅年,放射性物質(zhì)發(fā)生泄漏茧痒。R本人自食惡果不足惜肮韧,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一融蹂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弄企,春花似錦超燃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至约素,卻和暖如春届良,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背圣猎。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工士葫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人送悔。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓慢显,卻偏偏與公主長得像爪模,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子荚藻,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 最近看了很多RunLoop的文章屋灌,看完很懵逼,決心整理一下应狱,文章中大部分內(nèi)容都是引用大神們的共郭,但好歹對自己有個交代...
    小涼介閱讀 6,721評論 12 79
  • 概述 RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系,同時也是很多常見技術(shù)的幕后功臣疾呻。盡管在平時多...
    陽明先生_X自主閱讀 1,101評論 0 17
  • ios 常用的定時器有三種:NSTime落塑,CADisplayLink和GCD。 NsTimer // 參數(shù):Int...
    殿小七閱讀 845評論 0 2
  • iOS刨根問底-深入理解RunLoop 2017-05-08 10:35 by KenshinCui 概述 Run...
    mengjz閱讀 1,560評論 1 10
  • 神奇旅館|001 蘿卜和泊雅 這是一家叫做神奇的旅館罐韩,在這里面住著許多形態(tài)各異的人憾赁,對于住在這里的人而言,找尋人生...
    懶貓吃魚不吃飯閱讀 525評論 0 2