iOS 探討之 dispatch_source 定時器

闡述

前面已經介紹 CADisplayLink、mach_absolute_time 都可以在定時這塊進行封裝,當然NSTimer也是可以的,這次我就梳理一下 dispatch_source 版本的定時。

NSTimer 受 RunLoop 的影響, 由於 RunLoop 需要處理很多任務,所以其精度不高。 CADisplayLink精度低的原因類似,具體原因放在最後解釋。

如果我們對定時器的精度要求很高,可以考慮使用 dispatch_source 去實現。

它精度很高,系統會自動觸發,系統級別的源,並且不受RunLoopMode的影響。

 

探討

Dispatch Source 是BSD系統內核慣有功能kqueue的包裝,kqueue是在XNU內核中發生事件時在應用程序編程方執行處理的技術。它的CPU負荷非常小,儘量不佔用資源。當事件發生時, Dispatch Source 會在指定的Dispatch Queue中執行事件的處理。

 

Dispatch Source 種類:

1   DISPATCH_SOURCE_TYPE_DATA_ADD 變量增加

2   DISPATCH_SOURCE_TYPE_DATA_OR 變量 OR

3   DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口發送

4   DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收

5   DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 內存壓力 (注:iOS8後可用)

6   DISPATCH_SOURCE_TYPE_PROC 檢測到與進程相關的事件

7   DISPATCH_SOURCE_TYPE_READ 可讀取文件映像

8   DISPATCH_SOURCE_TYPE_SIGNAL 接收信號

9   DISPATCH_SOURCE_TYPE_TIMER 定時器

10 DISPATCH_SOURCE_TYPE_VNODE 文件系統有變更

11 DISPATCH_SOURCE_TYPE_WRITE 可寫入文件映像

 

定時器則是 DISPATCH_SOURCE_TYPE_TIMER 所對應技術處理。

代碼

    // OC版本
    // 任務執行所指定的隊列    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // 當前定時器源        
    self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 任務執行開始時間
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC);
    
    // 任務執行間隔時間
    uint64_t interval = 1.0 * NSEC_PER_SEC;

    // 給定時器源綁定開始時間、間隔時間以及容忍誤差時間
    dispatch_source_set_timer(self.timerSource, start, interval, 0);
    
    // 給定時器源綁定任務
    dispatch_source_set_event_handler(self.timerSource, ^{
        // 內部最好使用weak/strong修飾的self, 防止循環引用
        NSLog(@"----self.timer---");
    });
    
    // 啓動定時器源
    dispatch_resume(self.timerSource);
    // Swift版本
    // 任務執行所指定的隊列    
    fileprivate let timer: DispatchSourceTimer = DispatchSource.makeTimerSource()

    // 時鐘改變間隔(默認 1s)   
    fileprivate var clockInterval: TimeInterval = 1

    fileprivate init() {
        // 給定時器源綁定開始時間、間隔時間
        self.timer.schedule(deadline: .now()+self.clockInterval, repeating: self.clockInterval)
        self.timer.setEventHandler(qos: .utility, flags: DispatchWorkItemFlags.assignCurrentContext) { [weak self] in
            guard let wself = self else { return }
            // 所執行的任務

        }
        self.timer.resume()
    }

OC 版本 dispatch_source定時器進入後臺的時候會自動暫停(網上所說的開始時間用 dispatch_walltime(NULL, 0) 也不好使)。

Swift 版本進入後臺不會自動暫停。

 

注意項

1.  source 需要手動啓動

Dispatch Source 使用最多的就是用來實現定時器,source創建後默認是暫停狀態,需要手動調用 dispatch_resume啓動定會器。 dispatch_after只是封裝調用了dispatch source定時器,然後在回調函數中執行定義的block.

 

2.  循環引用

因爲 dispatch_source_set_event_handle回調是block,在添加到source的鏈表上時會執行copy並被source強引用,如果block裏持有了self,self又持有了source的話,就會引起循環引用。所以正確的方法是使用weak+strong或者提前調dispatch_source_cancel取消timer.

 

3.  resume、suspend 調用次數保持平衡

dispatch_resume 和 dispatch_suspend 調用次數需要平衡,如果重複調用 dispatch_resume則會崩潰,因爲重複調用會讓dispatch_resume 代碼裏if分支不成立,從而執行了 DISPATCH_CLIENT_CRASH(“Over-resume of an object”) 導致崩潰。

 

4.  source 創建與釋放時機

source在suspend狀態下,如果直接設置 source = nil 或者重新創建 source 都會造成 crash。正確的方式是在resume狀態下調用 dispatch_source_cancel(source)後再重新創建。

 

拓展

其它定時器不準的原因

1.  NSTimer

NSTimer 、 CFRunLoopTimerRef 之間是 toll-free bridged 的。

NSTimer 註冊到 RunLoop 後, RunLoop 會爲其重複的時間點註冊事件。 如: 10:00、10:10、10:20 這幾個時間點。

RunLoop 爲了節省資源,並不會在非常準確的時間點回調這個 NSTimer 事件。

NSTimer 有個屬性叫做 Tolerance (寬容度),標識了當前時間點所容許的最大誤差。

如果這個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。

 

2.  CADisplayLink 

CADisplayLink 是一個和屏幕刷新率一致的定時器 , 如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去,該有的事件回調則不會去觸發,會造成界面卡頓,時間也不太準。

 

參考資料

1.  https://blog.csdn.net/wang_Bo_JustOne/article/details/76147050

2.  https://xiaozhuanlan.com/topic/9481560732

3.  https://www.jianshu.com/p/faa6ffe4fac3

4.  https://www.jianshu.com/p/edbe946c8a11?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

5.  https://www.jianshu.com/p/137885b8c140?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章