回顧
在上篇博客已經(jīng)對GCD的調(diào)度組做了介紹和舉例應(yīng)用,還有對底層源碼的分析,那么本篇博客將對事件源dispatch_source進行分析!
iOS底層探索之多線程(六)—GCD源碼分析(sync 同步函數(shù)、async 異步函數(shù))
iOS底層探索之多線程(八)—GCD源碼分析(函數(shù)的同步性、異步性、單例)
iOS底層探索之多線程(九)—GCD源碼分析(柵欄函數(shù))
iOS底層探索之多線程(十一)—GCD源碼分析(調(diào)度組)
1.Dispatch Source 介紹
Dispatch Source是BSD系統(tǒng)內(nèi)核慣有功能kqueue的包裝,kqueue是在XNU內(nèi)核中發(fā)生事件時在應(yīng)用程序編程方執(zhí)行處理的技術(shù)。
它的CPU負荷非常小,盡量不占用資源。當(dāng)事件發(fā)生時,Dispatch Source會在指定的Dispatch Queue中執(zhí)行事件的處理。
-
dispatch_source_create:創(chuàng)建源 -
dispatch_source_set_event_handler: 設(shè)置源的回調(diào) -
dispatch_source_merge_data: 源事件設(shè)置數(shù)據(jù) -
dispatch_source_get_data: 獲取源事件的數(shù)據(jù) -
dispatch_resume:恢復(fù)繼續(xù) -
dispatch_suspend:掛起
我們在日常開發(fā)中,經(jīng)常會使用計時器NSTimer,例如發(fā)送短信的倒計時,或者進度條的更新。但是NSTimer需要加入到NSRunloop中,還受到mode的影響。收到其他事件源的影響,被打斷,當(dāng)滑動scrollView的時候,模式切換,定時器就會停止,從而導(dǎo)致timer的計時不準確。
GCD提供了一個解決方案dispatch_source來出來類似的這種需求場景。
- 時間較準確,
CPU負荷小,占用資源少 - 可以使用子線程,解決定時器跑在主線程上卡UI問題
- 可以暫停,繼續(xù),不用像
NSTimer一樣需要重新創(chuàng)建
2.Dispatch Source 使用
創(chuàng)建事件源的代碼:
// 方法聲明
dispatch_source_t dispatch_source_create(
dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t _Nullable queue);
// 實現(xiàn)過程
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
創(chuàng)建的時候,需要傳入兩個重要的參數(shù):
-
dispatch_source_type_t要創(chuàng)建的源類型 -
dispatch_queue_t事件處理的調(diào)度隊列
2.1 Dispatch Source 種類
- Dispatch Source 種類:
DISPATCH_SOURCE_TYPE_DATA_ADD變量增加DISPATCH_SOURCE_TYPE_DATA_OR變量ORDISPATCH_SOURCE_TYPE_DATA_REPLACE新獲得的數(shù)據(jù)值替換現(xiàn)有的DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口發(fā)送DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收DISPATCH_SOURCE_TYPE_MEMORYPRESSURE內(nèi)存壓力 (注:iOS8后可用)DISPATCH_SOURCE_TYPE_PROC檢測到與進程相關(guān)的事件DISPATCH_SOURCE_TYPE_READ可讀取文件映像DISPATCH_SOURCE_TYPE_SIGNAL接收信號DISPATCH_SOURCE_TYPE_TIMER定時器DISPATCH_SOURCE_TYPE_VNODE文件系統(tǒng)有變更DISPATCH_SOURCE_TYPE_WRITE可寫入文件映像
設(shè)計一個定時器舉例:
- 點擊屏幕開始
使用
dispatch_source的計時器,能夠暫停、開始,同時不受主線程影響,不會受UI事件的影響,所以它的計時是準確的。如下圖所示:

2.2 使用時注意事項
注意事項
- source 需要手動啟動
Dispatch Source 使用最多的就是用來實現(xiàn)定時器,source創(chuàng)建后默認是暫停狀態(tài),需要手動調(diào)用 dispatch_resume啟動定會器。 dispatch_after只是封裝調(diào)用了dispatch source定時器,然后在回調(diào)函數(shù)中執(zhí)行定義的block.
- 循環(huán)引用
因為 dispatch_source_set_event_handle回調(diào)是block,在添加到source的鏈表上時會執(zhí)行copy并被source強引用,如果block里持有了self,self又持有了source的話,就會引起循環(huán)引用。所以正確的方法是使用weak+strong或者提前調(diào)dispatch_source_cancel取消timer。
- resume、suspend 調(diào)用次數(shù)保持平衡
dispatch_resume和 dispatch_suspend調(diào)用次數(shù)需要平衡,如果重復(fù)調(diào)用 dispatch_resume則會崩潰,因為重復(fù)調(diào)用會讓dispatch_resume 代碼里if分支不成立,從而執(zhí)行了 DISPATCH_CLIENT_CRASH(“Over-resume of an object”)導(dǎo)致崩潰。
- source 創(chuàng)建與釋放時機
source在suspend狀態(tài)下,如果直接設(shè)置 source = nil或者重新創(chuàng)建 source 都會造成crash。正確的方式是在resume狀態(tài)下調(diào)用 dispatch_source_cancel(source)后再重新創(chuàng)建。
3. Dispatch Source源碼分析
那么去底層源碼看看,為什么會出現(xiàn)上面的一些問題。
3.1 dispatch_resume
- dispatch_resume
void
dispatch_resume(dispatch_object_t dou)
{
DISPATCH_OBJECT_TFB(_dispatch_objc_resume, dou);
if (unlikely(_dispatch_object_is_global(dou) ||
_dispatch_object_is_root_or_base_queue(dou))) {
return;
}
if (dx_cluster(dou._do) == _DISPATCH_QUEUE_CLUSTER) {
_dispatch_lane_resume(dou._dl, DISPATCH_RESUME);
}
}
dispatch_resume``會去執(zhí)行_dispatch_lane_resume
- _dispatch_lane_resume
這里的方法是對事件源的相關(guān)狀態(tài)進行判斷,如果過度
resume恢復(fù),則會goto走到over_resume流程,直接調(diào)起DISPATCH_CLIENT_CRASH崩潰。
這里還有對掛起計數(shù)的判斷,掛起計數(shù)包含所有掛起和非活動位的掛起計數(shù)。underflow下溢意味著需要過度恢復(fù)或暫停計數(shù)轉(zhuǎn)移到邊計數(shù),也就是說如果當(dāng)前計數(shù)器還沒有到可運行的狀態(tài),需要連續(xù)恢復(fù)。
3.2 dispatch_suspend
- 掛起dispatch_suspend
在
dispatch_suspend的定義里面也可以發(fā)現(xiàn),恢復(fù)和掛起一定要保持平衡,掛起的對象不會調(diào)用與其關(guān)聯(lián)的任何block。 在與對象關(guān)聯(lián)的任何運行的 block完成后,對象將被掛起。
void
dispatch_suspend(dispatch_object_t dou)
{
DISPATCH_OBJECT_TFB(_dispatch_objc_suspend, dou);
if (unlikely(_dispatch_object_is_global(dou) ||
_dispatch_object_is_root_or_base_queue(dou))) {
return;
}
if (dx_cluster(dou._do) == _DISPATCH_QUEUE_CLUSTER) {
return _dispatch_lane_suspend(dou._dl);
}
}
-
_dispatch_lane_suspend
_dispatch_lane_suspend - _dispatch_lane_suspend_slow
_dispatch_lane_suspend_slow
同樣這里維護一個暫停掛起的計數(shù)器,如果連續(xù)調(diào)用dispatch_suspend掛起方法,減法的無符號下溢可能發(fā)生,因為其他線程可能在我們嘗試獲取鎖時觸及了該值,或者因為另一個線程爭先恐后地執(zhí)行相同的操作并首先獲得鎖。
所以不能重復(fù)的掛起或者恢復(fù),一定要你一個我一個,你兩個我也兩個,保持一個balanced。
4.總結(jié)
- 使用定時器
NSTimer需要加入到NSRunloop,導(dǎo)致計數(shù)不準確,可以使用Dispatch Source來解決 -
Dispatch Source的使用,要注意恢復(fù)和掛起要平衡 -
source在suspend狀態(tài)下,如果直接設(shè)置source = nil或者重新創(chuàng)建source都會造成crash。正確的方式是在resume狀態(tài)下調(diào)用dispatch_source_cancel(source)后再重新創(chuàng)建。 - 因為
dispatch_source_set_event_handle回調(diào)是block,在添加到source的鏈表上時會執(zhí)行copy并被source強引用,如果block里持有了self,self又持有了source的話,就會引起循環(huán)引用。所以正確的方法是使用weak+strong或者提前調(diào)dispatch_source_cancel取消timer。
更多內(nèi)容持續(xù)更新
?? 喜歡就點個贊吧????
?? 覺得有收獲的,可以來一波,收藏+關(guān)注,評論 + 轉(zhuǎn)發(fā),以免你下次找不到我????
??歡迎大家留言交流,批評指正,互相學(xué)習(xí)??,提升自我??