Dispatch_group 與SDWebImage(一個奇怪的bug)

產品有個需求,需要下載一定數量的圖片,然后再執行相應操作。相信很多APP有這樣的需求場景,做起來也簡單,于是不加思考的代碼直接寫起來了(此為模擬代碼,和實際代碼邏輯基本一致)

    NSArray *imageURLArray = @[@"1", @"2", @"3", @"4"];
    dispatch_group_t group = dispatch_group_create();
    [imageURLArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        dispatch_group_enter(group);
        [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:imageURLArray[idx]] options:SDWebImageDownloaderLowPriority progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
            dispatch_group_leave(group);
            NSLog(@"idx:%zd",idx);

        }];
    }];
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"%@", imageURLArray);
    });

代碼邏輯可以說是很簡單:一個圖片URL數組。使用SDWebImage多線程進行并發下載,直到所有圖片都下載完成進行回調。但是就是這樣一段代碼居然會偶爾出現崩潰,和項目中其他地方使用到dispatch_group的地方進行過比較,也沒發現有什么不同,組內的同事都百思不得其解,沒辦法這時候只有先去看看dispatch_group的源碼了,其中有一段是這樣的

dispatch_group_leave(dispatch_group_t dg)
{
    dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg;
    dispatch_atomic_release_barrier();
    long value = dispatch_atomic_inc2o(dsema, dsema_value);
    if (slowpath(value == LONG_MIN)) {
        DISPATCH_CLIENT_CRASH("Unbalanced call to dispatch_group_leave()");
    }
    if (slowpath(value == dsema->dsema_orig)) {
        (void)_dispatch_group_wake(dsema);
    }
}

通過源代碼我們發現在調用dispatch_group_leave的時候是可能會發生crash的,這段代碼的重點就是當這個value值和LONG_MIN相等的時候,這里會發生crash。
提示:這里LONG_MIN表示這個類型的范圍內最小值,對應LONG_MAX表示最大值,slowpath用來提示編譯器優化,對應的還有fastpath

然后我們搞清楚value值是什么,這里傳進來一個dg,轉化成信號量dsema,然后調用dispatch_atomic_inc2o,這個函數的作用就是把dsema的value值加1,然后返回給value,所以value就是表示當前這個信號的信號量。所以簡單來說這段代碼的意思就是,如果當前的信號調用leave會判斷其信號量,如果信號量等于這個最小值就會crash。那對應的我們看看初始化函數

dispatch_group_create(void)
{
    return (dispatch_group_t)dispatch_semaphore_create(LONG_MAX);
}

dispatch_semaphore_create(long value)
{
    dispatch_semaphore_t dsema;

    if (value < 0) {
        return NULL;
    }
    dsema = calloc(1, sizeof(struct dispatch_semaphore_s));

    if (fastpath(dsema)) {
        dsema->do_vtable = &_dispatch_semaphore_vtable;
        dsema->do_next = DISPATCH_OBJECT_LISTLESS;
        dsema->do_ref_cnt = 1;
        dsema->do_xref_cnt = 1;
        dsema->do_targetq = dispatch_get_global_queue(
                DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dsema->dsema_value = value;
        dsema->dsema_orig = value;
#if USE_POSIX_SEM
        int ret = sem_init(&dsema->dsema_sem, 0, 0);
        DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif
    }

    return dsema;
}

這里實際上我們可以看到,當你create一個信號量的時候,這里默認賦值給它一個LONG_MAX。這里我們似乎就明白了,LONG_MAX調用leave然后+1,明顯溢出了,所以導致crash。我們可以用簡單的代碼驗證一下:

 dispatch_group_t group = dispatch_group_create();
 dispatch_group_leave(group);

不用說直接crash,但是我們代碼里并不是這樣的錯誤,那會是什么樣的原因導致溢出呢?我們再回顧一下代碼的邏輯

  1. 遍歷任務,enter了4次,信號量減去4次
  2. SDWebImage下載回調的時候,leave了4次,信號量增加了4次
  3. 完成任務

所以問題應該是出現在SDWebImage里,我們知道SDWebImage是異步下載,誰先下載完成是沒法保證的,但是在一個任務期間這個也是沒影響的,但是如果在一個任務期間沒執行完成,上述任務又循環了一次呢?這里我們模擬一下整個過程

  1. 第一次創建信號A,enter4次,A.Value = -4
  2. SD下載回來,leave4次,我們記為A(1),A(2),A(3),A(4)
  3. 循環一遍同理得到,B.Value = -4,B(1),B(2),B(3),B(4)

閱讀過SDWebImage源碼的人都知道其大概原理流程,SDWebImage下載器會根據URL做下載任務對應NSOperation映射,也就是之前創建的下載回調Block,所以這里都是一一對應的,試想一下:

如果A和B里面有URL相同的情況A(1)和B(1),這時候其中一個便會被替換掉,只會存在一個Block回調B(1),當A(1)和B(1)下載分別完成的時候,會調用同一個回調B(1),這時候就導致了B信號被多leave了一次。B enter4次,leave5次,導致上面所說的溢出crash。

最后問題找到了,其實原理挺簡單的,第三方庫的源碼一定要有所了解,才能避免這種突如其來的bug

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數據結構(3).初始化時...
    歐辰_OSR閱讀 29,547評論 8 265
  • 1.如何追蹤app崩潰率,如何解決線上閃退 當 iOS設備上的App應用閃退時,操作系統會生成一個crash日志,...
    中婭沙漏閱讀 593評論 0 5
  • Managing Units of Work(管理工作單位) 調度塊允許您直接配置隊列中各個工作單元的屬性。它們還...
    edison0428閱讀 8,032評論 0 1
  • 說明 popper是參考popper.js來實現浮動的工具,結構十分清晰明了,通過modifiers來處理數據的思...
    liril閱讀 30,683評論 4 19
  • 閑暇時,從柜子里不小心翻出一個小時候特別特別喜歡的玩具,那一刻突然覺得有點驚訝:它怎么會是這個樣子??顏色...
    儷人歸閱讀 421評論 1 2