iOS底層探索之多線程(十二)—GCD源碼分析(事件源dispatch_source)

回顧

在上篇博客已經(jīng)對GCD調(diào)度組做了介紹和舉例應(yīng)用,還有對底層源碼的分析,那么本篇博客將對事件源dispatch_source進行分析!

多線程

iOS底層探索之多線程(一)—進程和線程

iOS底層探索之多線程(二)—線程和鎖

iOS底層探索之多線程(三)—初識GCD

iOS底層探索之多線程(四)—GCD的隊列

iOS底層探索之多線程(五)—GCD不同隊列源碼分析

iOS底層探索之多線程(六)—GCD源碼分析(sync 同步函數(shù)、async 異步函數(shù))

iOS底層探索之多線程(七)—GCD源碼分析(死鎖的原因)

iOS底層探索之多線程(八)—GCD源碼分析(函數(shù)的同步性、異步性、單例)

iOS底層探索之多線程(九)—GCD源碼分析(柵欄函數(shù))

iOS底層探索之多線程(十)—GCD源碼分析( 信號量)

iOS底層探索之多線程(十一)—GCD源碼分析(調(diào)度組)

1.Dispatch Source 介紹

Dispatch SourceBSD系統(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 種類:
  1. DISPATCH_SOURCE_TYPE_DATA_ADD 變量增加

  2. DISPATCH_SOURCE_TYPE_DATA_OR 變量 OR

  3. DISPATCH_SOURCE_TYPE_DATA_REPLACE 新獲得的數(shù)據(jù)值替換現(xiàn)有的

  4. DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口發(fā)送

  5. DISPATCH_SOURCE_TYPE_MACH_RECV MACH 端口接收

  6. DISPATCH_SOURCE_TYPE_MEMORYPRESSURE內(nèi)存壓力 (注:iOS8后可用)

  7. DISPATCH_SOURCE_TYPE_PROC 檢測到與進程相關(guān)的事件

  8. DISPATCH_SOURCE_TYPE_READ 可讀取文件映像

  9. DISPATCH_SOURCE_TYPE_SIGNAL 接收信號

  10. DISPATCH_SOURCE_TYPE_TIMER 定時器

  11. DISPATCH_SOURCE_TYPE_VNODE 文件系統(tǒng)有變更

  12. DISPATCH_SOURCE_TYPE_WRITE 可寫入文件映像

設(shè)計一個定時器舉例:


創(chuàng)建定時器方法
  • 點擊屏幕開始

定時器控制方法

使用dispatch_source的計時器,能夠暫停、開始,同時不受主線程影響,不會受UI事件的影響,所以它的計時是準確的。如下圖所示:

運行結(jié)果.gif

2.2 使用時注意事項

注意事項

  1. 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.

  1. 循環(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

  1. resume、suspend 調(diào)用次數(shù)保持平衡

dispatch_resumedispatch_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)致崩潰。

  1. source 創(chuàng)建與釋放時機

sourcesuspend狀態(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

_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

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ù)掛起平衡
  • sourcesuspend狀態(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í)??,提升自我??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容