iOS GCD底層分析(3)--柵欄函數(shù)、信號(hào)量、調(diào)度組、事件源

前言

上片文章分析了GCD隊(duì)列和函數(shù)的使用方式、串行隊(duì)列和并發(fā)隊(duì)列的創(chuàng)建、同步函數(shù)和異步函數(shù)底層執(zhí)行流程、串行隊(duì)列的死鎖、GCD單例的實(shí)現(xiàn)流程等。這篇文章我們繼續(xù)探究dispatch_barrier柵欄函數(shù)、dispatch_semaphore信號(hào)量dispatch_group調(diào)度組、dispatch_source事件源等,將從使用和底層原理兩個(gè)角度去分析這些內(nèi)容。

準(zhǔn)備工作

1. 柵欄函數(shù)

1.1 常用的柵欄函數(shù)

  • dispatch_barrier_async
    前面的任務(wù)執(zhí)行完畢才會(huì)執(zhí)行barrier中的邏輯,以及barrier后加入隊(duì)列的任務(wù)。
  • dispatch_barrier_sync
    作用相同,但是會(huì)堵塞線程,影響后面的任務(wù)執(zhí)行。

區(qū)別:dispatch_barrier_syncdispatch_barrier_async的區(qū)別也就在于會(huì)不會(huì)阻塞當(dāng)前線程,同時(shí)需要注意的是,柵欄函數(shù)只能控制同一并發(fā)隊(duì)列。

1.2 柵欄函數(shù)的使用

自定義了一個(gè)并發(fā)隊(duì)列,并且添加3個(gè)異步函數(shù),加下面代碼:

- (void)demo{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    /* 1.異步函數(shù) */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"1");
    });

    /* 2. 異步函數(shù) */
    dispatch_async(concurrentQueue, ^{
        sleep(0.5);
        NSLog(@"2");
    });

//    // 柵欄函數(shù)
//    dispatch_barrier_async(concurrentQueue, ^{
//        NSLog(@"----%@-----", [NSThread currentThread]);
//    });

    /* 3. 異步函數(shù) */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"3");
    });

    // 4
    NSLog(@"4");
}

運(yùn)行結(jié)果還是很明確,因?yàn)樵撽?duì)列是一個(gè)并發(fā)隊(duì)列,并且是異步函數(shù),所以任務(wù)1任務(wù)2、任務(wù)3、任務(wù)4的執(zhí)行順序是混亂的。見(jiàn)下面運(yùn)行結(jié)果:

執(zhí)行結(jié)果

  • 添加?xùn)艡诤瘮?shù)dispatch_barrier_sync
    如果現(xiàn)在有一個(gè)需求,確保任務(wù)1任務(wù)2先執(zhí)行,才能執(zhí)行任務(wù)3,可以添加一個(gè)柵欄函數(shù),見(jiàn)下面代碼:

    添加dispatch_barrier_sync

    分析:
    任務(wù)1任務(wù)2一定會(huì)先于柵欄函數(shù)運(yùn)行,在柵欄函數(shù)運(yùn)行之后,才會(huì)運(yùn)行任務(wù)3。同時(shí)dispatch_barrier_sync還有另外一個(gè)特點(diǎn),會(huì)堵塞當(dāng)前的線程,所以任務(wù)4會(huì)在柵欄函數(shù)執(zhí)行后才會(huì)被執(zhí)行。

  • 添加?xùn)艡诤瘮?shù)dispatch_barrier_async

    添加dispatch_barrier_async

    分析:
    添加一個(gè)柵欄函數(shù)dispatch_barrier_async,運(yùn)行發(fā)現(xiàn),該并發(fā)隊(duì)列中的任務(wù)1任務(wù)2一定會(huì)先于柵欄函數(shù)運(yùn)行,在柵欄函數(shù)運(yùn)行之后,才會(huì)運(yùn)行任務(wù)3。因?yàn)?code>任務(wù)4是在主隊(duì)列,所以并不影響任務(wù)4的正常執(zhí)行。

  • 注意:

    • dispatch_barrier_sync會(huì)阻塞當(dāng)前線程
    • 柵欄函數(shù)和其他的任務(wù)必須在同一個(gè)隊(duì)列中
    • 不能使用全局并發(fā)隊(duì)列(后面會(huì)分析)

1.3 柵欄函數(shù)的底層原理

我們對(duì)柵欄函數(shù)的任務(wù)無(wú)非就是柵欄函數(shù)起到同步的作用全局并發(fā)隊(duì)列不能夠執(zhí)行柵欄函數(shù)。那我們分析一下源碼,看看源碼是怎么樣的邏輯,請(qǐng)往下走。
libdispatch.dylib源碼中全局搜索dispatch_barrier_sync,一路往下跟蹤最終找到了_dispatch_barrier_sync_f_inline方法中,如下圖:

_dispatch_barrier_sync_f_inline

通過(guò)下符號(hào)斷點(diǎn)_dispatch_sync_f_slow,成功進(jìn)入了該方法,說(shuō)明柵欄函數(shù)是進(jìn)入以上判斷的,如下圖:
下_dispatch_sync_f_slow符號(hào)斷點(diǎn)

_dispatch_sync_f_slow方法在之前同步函數(shù)執(zhí)行死鎖時(shí)候已經(jīng)分析過(guò),同時(shí)在調(diào)用這個(gè)方法時(shí)設(shè)置了DC_FLAG_BARRIER的標(biāo)簽。_dispatch_sync_f_slow方法見(jiàn)下圖:
_dispatch_sync_f_slow

因?yàn)?code>func基本不會(huì)為NULL,那我們添加_dispatch_sync_invoke_and_complete_recurse符號(hào)斷點(diǎn),發(fā)現(xiàn)的確進(jìn)入了這個(gè)方法,如下:
下_dispatch_sync_invoke_and_complete_recurse符號(hào)斷點(diǎn)

通過(guò)上面的運(yùn)行堆棧,發(fā)現(xiàn)其流程為:_dispatch_sync_f_slow -> _dispatch_sync_invoke_and_complete_recurse -> _dispatch_sync_complete_recurse,最終定位到_dispatch_sync_complete_recurse方法,見(jiàn)下圖:
_dispatch_sync_complete_recurse

分析:
柵欄函數(shù)的作用是起到同步,也就是說(shuō)隊(duì)列中之前的任務(wù)沒(méi)有執(zhí)行完,柵欄函數(shù)肯定是不會(huì)走的。所以在進(jìn)行柵欄函數(shù)調(diào)用之前,肯定是要進(jìn)行遞歸處理,完成隊(duì)列中的任務(wù)
_dispatch_sync_complete_recurse方法中,進(jìn)行了遞歸處理,如果當(dāng)前存在barrier,則會(huì)將當(dāng)前隊(duì)列中的任務(wù)全部喚醒執(zhí)行,調(diào)用dx_wakeup。喚醒執(zhí)行完畢后,才會(huì)執(zhí)行_dispatch_lane_non_barrier_complete,即當(dāng)前隊(duì)列任務(wù)已經(jīng)執(zhí)行完成了,并且沒(méi)有柵欄函數(shù),執(zhí)行下面的流程。

想要執(zhí)行柵欄函數(shù)之后的任務(wù)柵欄函數(shù)要先移除,那么柵欄函數(shù)在哪里被執(zhí)行或者被移除的呢?跟蹤dx_wakeup執(zhí)行流程。dx_wakeup是通過(guò)宏定義的函數(shù),全局搜索并找到了定義的位置,見(jiàn)下圖:

dx_wakeup

之前我們已經(jīng)說(shuō)過(guò),底層為不同類型的隊(duì)列提供不同的調(diào)用入口那為什么全局并發(fā)隊(duì)列不能夠用柵欄函數(shù)呢?繼續(xù)往下看!

自定義并發(fā)隊(duì)列
自定義并發(fā)隊(duì)列會(huì)調(diào)用_dispatch_lane_wakeup方法,定位源碼,見(jiàn)下圖:

_dispatch_lane_wakeup

首先會(huì)判斷是否為barrier形式,如果是,則會(huì)調(diào)用_dispatch_lane_barrier_complete方法,處理有柵欄函數(shù)的流程;如果沒(méi)有,則走正常的并發(fā)隊(duì)列流程,調(diào)用_dispatch_queue_wakeup方法。

進(jìn)入_dispatch_lane_barrier_complete方法,查看流程,如下:

_dispatch_lane_barrier_complete

分析:
如果是串行隊(duì)列,則會(huì)進(jìn)行等待,直到其他的任務(wù)執(zhí)行完成,按順序執(zhí)行;如果是并發(fā)隊(duì)列,則會(huì)調(diào)用_dispatch_lane_drain_non_barriers將柵欄之前的任務(wù)執(zhí)行完成。最終調(diào)用_dispatch_lane_class_barrier_complete方法,完成柵欄的清除,從而執(zhí)行柵欄之后的任務(wù)。

全局并發(fā)隊(duì)列
如果是全局并發(fā)隊(duì)列,dx_wakeup方法對(duì)應(yīng)的是_dispatch_root_queue_wakeup方法,查看_dispatch_root_queue_wakeup源碼實(shí)現(xiàn),見(jiàn)下圖:

_dispatch_root_queue_wakeup

在全局并發(fā)隊(duì)列流程中,并沒(méi)有柵欄函數(shù)的相關(guān)處理流程,也就是按照正常的并發(fā)隊(duì)列來(lái)處理
總結(jié):
全局并發(fā)隊(duì)列為什么沒(méi)有對(duì)柵欄函數(shù)進(jìn)行處理呢?因?yàn)槿植l(fā)隊(duì)列除了被我們使用,系統(tǒng)也在使用,如果添加了柵欄函數(shù),會(huì)導(dǎo)致隊(duì)列運(yùn)行的阻塞,從而影響系統(tǒng)級(jí)的運(yùn)行,所以柵欄函數(shù)也就不適用于全局并發(fā)隊(duì)列

2. 信號(hào)量

在使用GCD過(guò)程中我們也會(huì)用到信號(hào)量(Dispatch Semaphore),持有計(jì)數(shù)的信號(hào)。Dispatch Semaphore提供了三個(gè)函數(shù)。

  • dispatch_semaphore_create:創(chuàng)建一個(gè)Semaphore并初始化信號(hào)的總量
  • dispatch_semaphore_wait:可以使總信號(hào)量減1,當(dāng)信號(hào)總量為0時(shí)就會(huì)一直等待阻塞所在線程),否則就可以正常執(zhí)行
  • dispatch_semaphore_signal發(fā)送一個(gè)信號(hào),讓信號(hào)總量加1解鎖

查看dispatch_semaphore_createAPI相關(guān)說(shuō)明如下:

dispatch_semaphore_create

我們可以得出結(jié)論,信號(hào)量如果大于0,表示可以控制GCD的最大并發(fā)數(shù)。

2.1 信號(hào)量的使用

  • 案例1
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);

    //任務(wù)1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
        sleep(2);
        NSLog(@"執(zhí)行任務(wù)1");
        NSLog(@"任務(wù)1完成");
        dispatch_semaphore_signal(sem); // 發(fā)信號(hào)
    });

    // 任務(wù)2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待、

        sleep(2);
        NSLog(@"執(zhí)行任務(wù)2");
        NSLog(@"任務(wù)2完成");
        dispatch_semaphore_signal(sem); // 發(fā)信號(hào)
    });

全局并發(fā)隊(duì)列中,異步執(zhí)行相關(guān)的任務(wù),當(dāng)前Semaphore的初始值為1,也就是說(shuō)當(dāng)前隊(duì)列最大并發(fā)數(shù)為1。dispatch_semaphore_wait表示阻塞,或者說(shuō)占用一個(gè)信號(hào),dispatch_semaphore_signal表示釋放,也就是釋放所占用的信號(hào)。

  • 案例2
    對(duì)上面的案例進(jìn)行一些調(diào)整,我們將信號(hào)量初始值變?yōu)?code>0,也就是最大并發(fā)數(shù)設(shè)置為0。異步并發(fā)執(zhí)行兩個(gè)任務(wù),并且任務(wù)延遲了2秒鐘,見(jiàn)下面代碼:

    案例2

    如果沒(méi)有加入信號(hào)量的話,一般情況都會(huì)先執(zhí)行任務(wù)1然后再執(zhí)行任務(wù)2。但是實(shí)際的情況相反。這里dispatch_semaphore_wait加鎖的作用,而dispatch_semaphore_signal解鎖作用。當(dāng)執(zhí)行任務(wù)1時(shí),dispatch_semaphore_wait加鎖進(jìn)行等待,當(dāng)任務(wù)2執(zhí)行完畢后,dispatch_semaphore_signal解鎖發(fā)出信號(hào),其他的任務(wù)可以執(zhí)行,起到控制流程的作用。

  • 案例3
    信號(hào)量初始值變?yōu)?code>0,也就是最大并發(fā)數(shù)設(shè)置為0。dispatch_semaphore_wait在主線程中,異步流程中停頓2秒鐘,正常情況下應(yīng)該會(huì)先執(zhí)行打印操作,number輸出等于0才對(duì),但是實(shí)際的情況是number等于1。見(jiàn)下圖代碼:

    案例3

原因和案例2是一致的,dispatch_semaphore_wait加鎖阻塞了當(dāng)前線程,dispatch_semaphore_signal解鎖后當(dāng)前線程繼續(xù)執(zhí)行,number輸出結(jié)果為1。

2.2 信號(hào)量原理分析

我們探究原理肯定是帶著目的去的,那么我們以下就是主要探索dispatch_semaphore_waitdispatch_semaphore_signal加鎖和解鎖功能是如何實(shí)現(xiàn)的,跟著走吧。

2.2.1 dispatch_semaphore_wait原理

libdispatch.dyld中查找其實(shí)現(xiàn)源碼如下:

dispatch_semaphore_wait

分析:
os_atomic_dec2o進(jìn)行減操作,也就是對(duì)創(chuàng)建是傳入的value值進(jìn)行減操作。以此來(lái)控制可并發(fā)數(shù)。
如果可并發(fā)數(shù)為3,則調(diào)用該方法后,變?yōu)?code>2,表示占用一個(gè)并發(fā)數(shù),剩下還可同時(shí)執(zhí)行2個(gè)任務(wù)。但是,如果初始值是0,減操作之后為負(fù)數(shù),則會(huì)調(diào)動(dòng)_dispatch_semaphore_wait_slow方法。

_dispatch_semaphore_wait_slow實(shí)現(xiàn)如下:

_dispatch_semaphore_wait_slow

上面的案例中我們調(diào)用dispatch_semaphore_wait時(shí),傳入的flagDISPATCH_TIME_FOREVER,表示一直等待。進(jìn)入_dispatch_sema4_wait實(shí)現(xiàn)流程,如下圖:
_dispatch_sema4_wait

分析:
由上圖看出_dispatch_sema4_wait的實(shí)現(xiàn)是在lock(鎖)的相關(guān)文件,可以知道_dispatch_sema4_wait是對(duì)鎖進(jìn)行操作的。
_dispatch_sema4_wait進(jìn)行do-while循環(huán),當(dāng)不滿足條件時(shí),會(huì)一直循環(huán)下去,從而導(dǎo)致流程的阻塞。這也就解釋了上面案例2案例3的執(zhí)行結(jié)果。

2.2.2 dispatch_semaphore_signal原理

其實(shí)現(xiàn)代碼如下:

dispatch_semaphore_signal

os_atomic_inc2o加操作,也就是對(duì)可用并發(fā)數(shù)據(jù)進(jìn)行釋放,將dispatch_semaphore_wait獲取的一個(gè)執(zhí)行權(quán)限釋放掉。
當(dāng)信號(hào)量初始值是0時(shí),調(diào)用加操作后,value值大于0,這樣就可以獲得執(zhí)行權(quán)限。但是如果加一次后依然小于0,則會(huì)報(bào)異常:Unbalanced call to dispatch_semaphore_signal()。并調(diào)用_dispatch_semaphore_signal_slow方法。
_dispatch_semaphore_signal_slow實(shí)現(xiàn)如下:
_dispatch_semaphore_signal_slow

_dispatch_sema4_signal同樣會(huì)開(kāi)啟一個(gè)do-while循環(huán),直到滿足條件可以運(yùn)行為止。
_dispatch_sema4_signal

Dispatch Semaphore總結(jié):

  • 保持線程同步,將異步執(zhí)行任務(wù)轉(zhuǎn)換為同步執(zhí)行任務(wù)
  • 保證線程安全,為線程加鎖

3. 調(diào)度組

dispatch_group,主要作用是控制任務(wù)的執(zhí)行順序。提供了以下方法:

  • dispatch_group_create 創(chuàng)建組
  • dispatch_group_async 進(jìn)組任務(wù)并執(zhí)行
  • dispatch_group_notify 進(jìn)組任務(wù)執(zhí)行完畢通知
  • dispatch_group_wait 進(jìn)組任務(wù)執(zhí)行等待時(shí)間
  • dispatch_group_enter 進(jìn)組
  • dispatch_group_leave 出組

注意:dispatch_group_enterdispatch_group_leave必須要成對(duì)使用

3.1 調(diào)度組的使用

  • 調(diào)度組案例
    要求完成任務(wù)1、任務(wù)2、任務(wù)3之后才能執(zhí)行任務(wù)4。使用調(diào)度組可以采用以下方式:

    調(diào)度組案例

    各個(gè)queue加到group里,然后當(dāng)組中任務(wù)完成后再調(diào)用任務(wù)4,這里使用了dispatch_group_wait進(jìn)行等待。dispatch_group_wait()函數(shù)會(huì)一直等到前面group中的內(nèi)容執(zhí)行完再執(zhí)行下面內(nèi)容,但會(huì)產(chǎn)生阻塞線程的問(wèn)題。這也就導(dǎo)致了主線程中的任務(wù)5不能正常運(yùn)行,直到任務(wù)組的任務(wù)完成才能被調(diào)用。

  • dispatch_group_notify的使用
    為解決上面的問(wèn)題,可采用dispatch_group_notify進(jìn)行任務(wù)執(zhí)行完畢的通知,見(jiàn)下圖:

    dispatch_group_notify的使用

    采用這種方式后,任務(wù)5不會(huì)被阻塞,當(dāng)任務(wù)組中的任務(wù)執(zhí)行完畢后,再通知任務(wù)4執(zhí)行。

  • 進(jìn)組出組的使用
    dispatch_group_enterdispatch_group_leave搭配使用也可以完成上面的效果,見(jiàn)下圖:

    進(jìn)出組的使用

    一個(gè)enter必須對(duì)應(yīng)一個(gè)leave成對(duì)出現(xiàn)!當(dāng)所有任務(wù)都執(zhí)行完成并出組后,才會(huì)執(zhí)行任務(wù)4,并且不會(huì)阻塞任務(wù)5的執(zhí)行

如果enterleave沒(méi)有成對(duì)出現(xiàn),比如多了一個(gè)leave則會(huì)崩潰,見(jiàn)下圖:

崩潰案例

如果多一個(gè)進(jìn)組enter,則后續(xù)的任務(wù)則不能正常運(yùn)行。見(jiàn)下圖:
不能運(yùn)行案例

3.2 調(diào)度組底層原理分析

dispatch_group_enter進(jìn)組和dispatch_group_leave出組為什么能夠起到與調(diào)度組dispatch_group_async一樣的效果呢?

  • dispatch_group_create
    dispatch_group_create方法實(shí)現(xiàn)見(jiàn)下圖:

    dispatch_group_create

    會(huì)調(diào)用_dispatch_group_create_with_count方法,并默認(rèn)傳入0,_dispatch_group_create_with_count的實(shí)現(xiàn)見(jiàn)下圖:
    _dispatch_group_create_with_count

    通過(guò)os_atomic_store2o進(jìn)行保存。

  • dispatch_group_enter
    查看dispatch_group_enter實(shí)現(xiàn)源碼,見(jiàn)下圖:

    dispatch_group_enter

    os_atomic_sub_orig2o會(huì)進(jìn)行--減減操作,此時(shí)的old_bits等于-1。

  • ** dispatch_group_leave**
    查看dispatch_group_leave實(shí)現(xiàn)源碼,見(jiàn)下圖:

    dispatch_group_leave

    這里通過(guò)os_atomic_add_orig2o,++加加操作獲取了old_state,此時(shí)old_state就等于0。而0&DISPATCH_GROUP_VALUE_MASK依然等于0,也就是old_value等于0。與此同時(shí),DISPATCH_GROUP_VALUE_1的定義見(jiàn)下面代碼:

#define DISPATCH_GROUP_VALUE_MASK       0x00000000fffffffcULL
#define DISPATCH_GROUP_VALUE_1          DISPATCH_GROUP_VALUE_MASK
#define DISPATCH_GROUP_VALUE_MASK       0x00000000fffffffcULL

很顯然old_value不等于DISPATCH_GROUP_VALUE_MASK的,所以流程會(huì)進(jìn)入到外層的if中,并調(diào)用_dispatch_group_wake方法進(jìn)行喚醒,喚醒的就是dispatch_group_notify方法,也就是說(shuō),如果不調(diào)用dispatch_group_leave方法,也就不會(huì)喚醒dispatch_group_notify,下面的流程也就不會(huì)執(zhí)行。

  • dispatch_group_notify
    查看dispatch_group_notify源碼發(fā)現(xiàn),在old_state等于0的情況下,才會(huì)去喚醒相關(guān)的同步異步函數(shù)執(zhí)行流程。見(jiàn)下圖:

    dispatch_group_notify

    dispatch_group_leave分析中,我們已經(jīng)得到old_state結(jié)果等于0
    所以這里也就解釋了dispatch_group_enterdispatch_group_leave為什么要配合起來(lái)使用的原因,通過(guò)信號(hào)量的控制,避免異步的影響,能夠及時(shí)喚醒并調(diào)用dispatch_group_notify方法。

  • dispatch_group_async的封裝
    為什么說(shuō)dispatch_group_async就等于dispatch_group_enterdispatch_group_leave呢?一起探究一下dispatch_group_async封裝。
    dispatch_group_async的定義,見(jiàn)下圖:

    dispatch_group_async

    進(jìn)入_dispatch_continuation_group_async方法如下:
    _dispatch_continuation_group_async

    在調(diào)用dispatch_group_async方法向組中添加任務(wù)時(shí),就調(diào)用了dispatch_group_enter方法,將信號(hào)量0變成了-1。
    那么如果需要將信號(hào)量重置,一定是在任務(wù)執(zhí)行完畢后再調(diào)用dispatch_group_leave方法。繼續(xù)跟蹤代碼,調(diào)用_dispatch_continuation_async方法,其源碼實(shí)現(xiàn)見(jiàn)下圖:
    _dispatch_continuation_async

    又回到了異步函數(shù)的流程了!具體異步函數(shù)分析過(guò)程見(jiàn)iOS GCD底層分析(1),這里不再跟蹤分析。

異步函數(shù)最終會(huì)調(diào)用_dispatch_worker_thread2方法,那么我們查看堆棧信息得到如下:

隊(duì)列組堆棧信息

跟蹤流程會(huì)調(diào)用_dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline方法。

先進(jìn)入_dispatch_root_queue_drain方法,如下:

_dispatch_root_queue_drain

跟蹤進(jìn)入到_dispatch_continuation_pop_inline方法,如下:
_dispatch_continuation_pop_inline

跟蹤進(jìn)入到_dispatch_continuation_invoke_inline方法,如下:
_dispatch_continuation_invoke_inline

跟蹤進(jìn)去_dispatch_continuation_with_group_invoke方法,如下:
_dispatch_continuation_with_group_invoke

在這里完成_dispatch_client_callout函數(shù)調(diào)用后,緊接著調(diào)用dispatch_group_leave方法,將信號(hào)量由-1變成了0。

注意:到此已經(jīng)完整的分析了調(diào)度組進(jìn)組、出組、通知的底層原理和關(guān)系。

4. 事件源

在日常的開(kāi)過(guò)程中,我們經(jīng)常會(huì)用到NSTimer。NSTimer需要加入到NSRunloop中,還受到mode的影響。在mode設(shè)置不對(duì)的情況下,scrollView滑動(dòng)的時(shí)候NSTimer也會(huì)收到影響。如果Runloop正在進(jìn)行連續(xù)性的運(yùn)行,timer就可能會(huì)被延遲。

GCD提供了一個(gè)解決方案dispatch_source源。dispatch_source有以下幾種特性:

  • 時(shí)間較準(zhǔn)確,CPU負(fù)荷小,占用資源少
  • 可以使用子線程,解決定時(shí)器跑在主線程上卡UI問(wèn)題
  • 可以暫停,繼續(xù),不用像NSTimer一樣需要重新創(chuàng)建

dispatch_source源的關(guān)鍵方法:

  • 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 繼續(xù)
  • dispatch_suspend 掛起

4.1 事件源的使用

  • 創(chuàng)建事件源
// 方法聲明
dispatch_source_t dispatch_source_create(
        dispatch_source_type_t type,
        uintptr_t handle,
        unsigned long mask,
        dispatch_queue_t _Nullable queue);

// 實(shí)現(xiàn)過(guò)程
dispatch_source_t source =  dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,  dispatch_get_main_queue());

創(chuàng)建過(guò)程需要傳入兩個(gè)重要的參數(shù):

  • dispatch_source_type_t 要?jiǎng)?chuàng)建的源類型
  • dispatch_queue_t 事件處理程序塊將提交到的調(diào)度隊(duì)列

事件源類型:

  • DISPATCH_SOURCE_TYPE_DATA_ADD 用于合并數(shù)據(jù)

  • DISPATCH_SOURCE_TYPE_DATA_OR 按位OR用于合并數(shù)據(jù)

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

  • DISPATCH_SOURCE_TYPE_MACH_SEND 監(jiān)視Mach端口的調(diào)度源,只有發(fā)送權(quán),沒(méi)有接收權(quán)
    -DISPATCH_SOURCE_TYPE_MACH_RECV 監(jiān)視Mach端口的待處理消息

  • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 監(jiān)控系統(tǒng)的變化,內(nèi)存壓力狀況

  • DISPATCH_SOURCE_TYPE_PROC 監(jiān)視外部進(jìn)程的事件的調(diào)度源

  • DISPATCH_SOURCE_TYPE_READ 監(jiān)控文件描述符的調(diào)度源可供讀取的字節(jié)

  • DISPATCH_SOURCE_TYPE_SIGNAL 用于監(jiān)視當(dāng)前進(jìn)程的信號(hào)

  • DISPATCH_SOURCE_TYPE_TIMER 基于計(jì)時(shí)器的調(diào)度源

  • DISPATCH_SOURCE_TYPE_VNODE 監(jiān)視事件文件描述符的調(diào)度源

  • DISPATCH_SOURCE_TYPE_WRITE 監(jiān)視事件,寫入字節(jié)的緩沖區(qū)空間

  • 事件源案例
    使用dispatch_source設(shè)計(jì)一個(gè)計(jì)時(shí)器,1秒鐘執(zhí)行一次,能夠暫停、開(kāi)始,同時(shí)不受主線程影響。見(jiàn)下圖實(shí)現(xiàn)代碼:

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) NSUInteger souceComplete;
@property (nonatomic) BOOL isRunning;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.souceComplete = 0;
    
    // 開(kāi)始時(shí)間
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
    // 間隔時(shí)間
    uint64_t interval = 1.0 * NSEC_PER_SEC;
    
    // source
    self.queue = dispatch_queue_create("test", NULL);
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    
    // 設(shè)置計(jì)時(shí)器
    dispatch_source_set_timer(self.source, start, interval, 0);

    __weak __typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(self.source, ^{
        NSLog(@"source --- %lu    ------  %@", (unsigned long)weakSelf.souceComplete++, [NSThread currentThread]);
    });

    // 默認(rèn)啟動(dòng)
    self.isRunning = YES;
    dispatch_resume(self.source);
}

// 計(jì)時(shí)器控制
- (IBAction)didClickStartOrPauseAction:(id)sender {
    if (self.isRunning) {
        dispatch_suspend(self.source);
        dispatch_suspend(self.queue);
        self.isRunning = NO;
        [sender setTitle:@"暫停中.." forState:UIControlStateNormal];
    }else{
        dispatch_resume(self.source);
        dispatch_resume(self.queue);
        self.isRunning = YES;
        [sender setTitle:@"計(jì)時(shí)中.." forState:UIControlStateNormal];
    }
}

@end
  • 運(yùn)行案例

    事件源案例

  • 注意事項(xiàng):

  • Dispatch Source Timer間隔定時(shí)器,也就是說(shuō)每隔一段時(shí)間間隔定時(shí)器就會(huì)觸發(fā)。在NSTimer中要做到同樣的效果需要手動(dòng)把repeats設(shè)置為 YES。

  • dispatch_source_set_timer中第二個(gè)參數(shù),當(dāng)我們使用dispatch_time或者DISPATCH_TIME_NOW時(shí),系統(tǒng)會(huì)使用默認(rèn)時(shí)鐘來(lái)進(jìn)行計(jì)時(shí)。然而當(dāng)系統(tǒng)休眠的時(shí)候,默認(rèn)時(shí)鐘是不走的,也就會(huì)導(dǎo)致計(jì)時(shí)器停止。使用dispatch_walltime可以讓計(jì)時(shí)器按照真實(shí)時(shí)間間隔進(jìn)行計(jì)時(shí)。

  • dispatch_source_set_timer的第四個(gè)參數(shù)leeway指的是一個(gè)期望的容忍時(shí)間,將它設(shè)置為1秒,意味著系統(tǒng)有可能在定時(shí)器時(shí)間到達(dá)的前1秒或者后1秒才真正觸發(fā)定時(shí)器。在調(diào)用時(shí)推薦設(shè)置一個(gè)合理的leeway值。需要注意,就算指定leeway值為0,系統(tǒng)也無(wú)法保證完全精確的觸發(fā)時(shí)間,只是會(huì)盡可能滿足這個(gè)需求

  • event handler block中的代碼會(huì)在指定的queue中執(zhí)行。當(dāng)queue是后臺(tái)線程的時(shí)候,dispatch timer相比NSTimer就好操作一些了。因?yàn)?code>NSTimer是需要Runloop支持的,如果要在后臺(tái)dispatch queue中使用,則需要手動(dòng)添加Runloop。使用dispatch timer就簡(jiǎn)單很多了。

  • dispatch_source_set_event_handler這個(gè)函數(shù)在執(zhí)行完之后,block會(huì)立馬執(zhí)行一遍,后面隔一定時(shí)間間隔再執(zhí)行一次。而NSTimer第一次執(zhí)行是到計(jì)時(shí)器觸發(fā)之后。這也是和NSTimer之間的一個(gè)顯著區(qū)別。

  • 停止source
    停止Dispatch Source有兩種方法,但是這兩種方式在使用時(shí)有很大的區(qū)別:

    • dispatch_suspend
    • dispatch_source_cancel

使用dispatch_suspend時(shí),source本身的實(shí)例需要一直保持dispatch_suspend之后的source,是不能被釋放的,如果釋放會(huì)崩潰,見(jiàn)下圖:

釋放source案例

使用dispatch_source_cancel則沒(méi)有這個(gè)限制,dispatch_source_cancel是真正意義上的取消source。被取消之后如果想再次執(zhí)行source,只能重新創(chuàng)建新的source。這個(gè)過(guò)程類似于對(duì)NSTimer執(zhí)行invalidate。見(jiàn)下圖:
cance案例

  • source掛起計(jì)數(shù)說(shuō)明
    dispatch_suspend嚴(yán)格上只是把source暫時(shí)掛起,它和dispatch_resume是一個(gè)平衡調(diào)用,兩者分別會(huì)減少增加dispatch對(duì)象的掛起計(jì)數(shù)。當(dāng)這個(gè)計(jì)數(shù)大于0的時(shí)候,source就會(huì)執(zhí)行。在掛起期間,產(chǎn)生的事件會(huì)積累起來(lái),等到dispatch_resume的時(shí)候會(huì)融合為一個(gè)事件發(fā)送。
  1. 重復(fù)啟動(dòng)一個(gè)正在執(zhí)行的源會(huì)崩潰


    重復(fù)執(zhí)行源
  2. 連續(xù)掛起,同樣需要連續(xù)對(duì)應(yīng)次數(shù)的啟動(dòng)才能夠正常運(yùn)行
    連續(xù)掛起source

    注意:dispatch source并沒(méi)有提供用于檢測(cè)source本身的掛起計(jì)數(shù)的API,也就是說(shuō)外部不能得知一個(gè)source當(dāng)前是不是掛起狀態(tài),在設(shè)計(jì)代碼邏輯時(shí)需要考慮到這兩點(diǎn)。

4.2 事件源底層原理分析

通常我們分析原理都是帶著問(wèn)題觸發(fā)的,那么這次我們探索根據(jù)以上的問(wèn)題:為什么source在運(yùn)行時(shí),重復(fù)調(diào)用dispatch_resume方法就會(huì)崩潰?在以下我們看看底層原理就一清二楚了。

查找dispatch_resume的底層實(shí)現(xiàn)原理,如下圖:

dispatch_resume

接著進(jìn)去_dispatch_lane_resume方法查看源碼,如下:
重復(fù)resume

重復(fù)resume直接進(jìn)入到了over_resume方法里面,查看其實(shí)現(xiàn)如下:
over_resume

通過(guò)解讀源碼發(fā)現(xiàn),底層會(huì)對(duì)事件源的相關(guān)狀態(tài)進(jìn)行判斷,如果其進(jìn)行過(guò)度恢復(fù),則會(huì)走到over_resume流程,直接調(diào)起DISPATCH_CLIENT_CRASH崩潰。
同時(shí)這里還維護(hù)了掛起計(jì)數(shù)(old_state),掛起計(jì)數(shù)包含所有掛起和非活動(dòng)位的掛起計(jì)數(shù)。下溢意味著需要過(guò)度恢復(fù)或暫停計(jì)數(shù)轉(zhuǎn)移到邊計(jì)數(shù),也就是說(shuō)如果當(dāng)前計(jì)數(shù)器還沒(méi)有到可運(yùn)行的狀態(tài),需要連續(xù)恢復(fù)。

  • 連續(xù)掛起
    我們發(fā)現(xiàn),連續(xù)掛起后需要對(duì)應(yīng)次數(shù)的恢復(fù)過(guò)程才能執(zhí)行,那么底層肯定是維護(hù)了一個(gè)信號(hào)量。首先搜索dispatch_suspend的實(shí)現(xiàn),見(jiàn)下圖:
    dispatch_suspend

    接著進(jìn)去_dispatch_lane_suspend方法查看源碼的實(shí)現(xiàn),如下:
    _dispatch_lane_suspend

    通過(guò)下符號(hào)斷點(diǎn)發(fā)現(xiàn)會(huì)進(jìn)入_dispatch_lane_suspend_slow的流程,源碼實(shí)現(xiàn)如下:
    _dispatch_lane_suspend_slow

    果不其然,同樣這里維護(hù)一個(gè)暫停計(jì)數(shù),如果連續(xù)調(diào)用掛起方法,則會(huì)進(jìn)行減法的無(wú)符號(hào)下溢

總結(jié)

花了不少的時(shí)間,GCD的探索就到此結(jié)束了,過(guò)程好艱辛但是收獲也是滿滿的。iOS的底層學(xué)習(xí)任重而道遠(yuǎn),繼續(xù)努力。

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

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

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