产品有个需求,需要下载一定数量的图片,然后再执行相应操作。相信很多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