iOS UI 操作在主线程不一定安全?

问题

最近在看SDWebImage的时候看到了他如何强行保护 UI 操作放置在主线程中执行,代码如下:

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

顿时心生疑问,按照我自己的写法,不应该这样么:

if ([NSThread isMainThread]) {
       block();
} else {
   dispatch_async(dispatch_get_main_queue(), ^{
       block();
   })
}

在查阅一阵子之后,没想到居然是真的。。。在 ReactiveCocoa 的一个 issue里提到在MapKit 中的 MKMapView 有个 addOverlay 方法,这个方法不仅要在主线程中执行,而且要把这个操作加到主队列中才可以。并且后来 Apple DTS 也承认了这是一个bug

ADTS Bug

由此,我们可以大胆的猜测,苹果的API可能是问题的,我们得想一个更加安全的方式规避这种即使有此类bug,万一别的API也有这样的问题也不至于导致APP出问题

解决方案

我们知道,在主队列中的任务,一定会放到主线程执行; 所以只要是在主队列中的任务,既可以保证在主队列,也可以保证在主线程中执行。所以咱们就可以通过判断当前队列是不是主队列来代替判断当前执行任务的线程是否是主线程,这样更加安全!

方案一:
我们知道在使用 GCD 创建一个 queue 的时候会指定 queue_label,可以理解为队列名,就像下面:

dispatch_queue_t myQueue = dispatch_queue_create("com.apple.threadQueue", DISPATCH_QUEUE_SERIAL);

而第一个参数就是 queue_label,根据官方文档解释,这个queueLabel应该是唯一的,所以SD就采用了这个方式

   //取得当前队列的队列名
   dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)
   
   //取得主队列的队列名
   dispatch_queue_get_label(dispatch_get_main_queue())
   
   然后通过 strcmp 函数进行比较,如果为0 则证明当前队列就是主队列。

正常情况下是可以这样来判断当前队列是不是主队列的,但考虑下面一种情况


//我定义了一个和主队列一样队列名的队列,通过这个判断,很明显判断成了主队列,于是我在里面做UI操作。

- (void)testQueue {
    //获取主队列名
    const char *main_queue_name = dispatch_queue_get_label(dispatch_get_main_queue());
    NSLog(@"\nmain_queue_name====%s", main_queue_name);
    //创建一个和主队列名字一样的串行队列
    dispatch_queue_t customSerialQueue = dispatch_queue_create(main_queue_name, DISPATCH_QUEUE_SERIAL);
    if (strcmp(dispatch_queue_get_label(customSerialQueue), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {
        //名字一样
        NSLog(@"\ncutomSerialQueue is main queue");
        dispatch_async(customSerialQueue, ^{
            //将更新UI的操作放到这个队列
            if ([NSThread isMainThread]) {
                NSLog(@"i am mainThread ");
            }
            UIImageView *imageView = [[UIImageView alloc] init];
            [self.view addSubview:imageView];
            self.view.frame = CGRectMake(0, 0, 50, 50);
            self.view.backgroundColor = [UIColor greenColor];
            NSLog(@"\nUI Action Finished");
        });
        
    } else {
        //名字不一样
        NSLog(@"cutomSerialQueue is main queue");
    }
}

所以,我想表达,如果我定义了一个这样的队列,并且当前队列就是这个队列,然后我再把 SD 设置图片的操作加到这个队列里面,这样会不会导致 SD 误判了,导致程序出问题,虽然这样很极限。如果是我理解错了,还请大神们悉心提出,求轻喷~~~~

执行结果:


result

方案二
采用 dispatch_queue_set_specificdispatch_get_specific 这一组方法为队列绑定标记,后面再取标记对比;当然你在这样的情况下,就别把自己的队列也和主队列打同样的标记了,不然就是在搞事情了。。。。

// 通过设置key/value数据与指定的queue进行关联。
dispatch_queue_set_specific(dispatch_queue_t queue, const void *key,
void *_Nullable context, dispatch_function_t _Nullable destructor);
//参数:
queue:需要关联的queue,不允许传入NULL。
key:唯一的关键字。
context:要关联的内容,可以为NULL。
destructor:释放context的函数,当新的context被设置时,destructor会被调用

// 根据唯一的key取出当前queue的context,如果当前queue没有key对应的context,则去queue的target queue取,取不着返回NULL,如果对全局队列取,也会返回NULL。
dispatch_get_specific(const void *key)

//参数
key:当时设置的关键字。

使用方式:

- (void)function {
    static void *mainQueueKey = "mainQueueKey";
    dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, &mainQueueKey, NULL);
    if (dispatch_get_specific(mainQueueKey)) {
        // do something in main queue
        //通过这样判断,就可以真正保证(我们在不主动搞事的情况下),任务一定是放在主队列中的
    } else {
        // do something in other queue
    }
}

总结

本文就从阅读 SD 源码中的一段代码而引发出的对主队列,主线程的一些思考;如有不正确,还请大神指导,轻喷~~

参考资料

主线程中也不绝对安全的 UI 操作
GCD's Main Queue vs. Main Thread

推荐阅读更多精彩内容

  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,020评论 1 17
  • 系统介绍: 1.系统采用主流的 SSM 框架 jsp JSTLbootstrap html5 (PC浏览器使用) ...
    5d7402928382阅读 122评论 0 0
  • 今天是二零一五年十个月九号,星期五。我在北京,这里天气很好。因为线路维修,全校停电停水,上面下达通知说是因为停电缘...
    莫莫莫呀阅读 141评论 1 2
  • 天 阴了 暗示着暴风雨要来临了 同时也暗示着挫折与失败 将要来临了 你,将如何选择 是退缩还是继续 退缩 那你将是...
    景佳琪阅读 63评论 0 0