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

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,353评论 8 265
  • 1.如何追踪app崩溃率,如何解决线上闪退 当 iOS设备上的App应用闪退时,操作系统会生成一个crash日志,...
    中娅沙漏阅读 578评论 0 5
  • Managing Units of Work(管理工作单位) 调度块允许您直接配置队列中各个工作单元的属性。它们还...
    edison0428阅读 7,954评论 0 1
  • 说明 popper是参考popper.js来实现浮动的工具,结构十分清晰明了,通过modifiers来处理数据的思...
    liril阅读 30,503评论 4 19
  • 闲暇时,从柜子里不小心翻出一个小时候特别特别喜欢的玩具,那一刻突然觉得有点惊讶:它怎么会是这个样子??颜色...
    俪人归阅读 414评论 1 2