闡述
前面已經介紹 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