產品有個需求,需要下載一定數量的圖片,然后再執行相應操作。相信很多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,但是我們代碼里并不是這樣的錯誤,那會是什么樣的原因導致溢出呢?我們再回顧一下代碼的邏輯
- 遍歷任務,enter了4次,信號量減去4次
- SDWebImage下載回調的時候,leave了4次,信號量增加了4次
- 完成任務
所以問題應該是出現在SDWebImage里,我們知道SDWebImage是異步下載,誰先下載完成是沒法保證的,但是在一個任務期間這個也是沒影響的,但是如果在一個任務期間沒執行完成,上述任務又循環了一次呢?這里我們模擬一下整個過程
- 第一次創建信號A,enter4次,A.Value = -4
- SD下載回來,leave4次,我們記為A(1),A(2),A(3),A(4)
- 循環一遍同理得到,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