从最初开始学习 iOS 的时候,我们就被告知 UI 操作一定要放在主线程进行。这是因为 UIKit 的方法不是线程安全的,保证线程安全需要极大的开销。那么问题来了,在主线程中进行 UI 操作一定是安全的么?

显然,答案是否定的!

在苹果的 MapKit 框架中,有一个叫做 addOverlay 的方法,它在底层实现的时候,不仅仅要求代码执行在主线程上,还要求执行在 GCD 的主队列上。这是一个极罕见的问题,但已经有人在使用 ReactiveCocoa 时踩到了坑,并提交了 issue。

苹果的 Developer Technology Support 承认这是一个 bug。不管这是 bug 还是历史遗留设计,也不管是不是在钻牛角尖,为了避免再次掉进同样的坑,我认为都有必要分析一下问题发生的原因和解决方案。

GCD 知识复习

在 GCD 中,使用 dispatch_get_main_queue() 函数可以获取主队列。调用 dispatch_sync() 方法会把任务同步提交到指定的队列。

注意一下队列和线程的区别,他们之间并没有“拥有关系(ownership)”,当我们同步的提交一个任务时,首先会阻塞当前队列,然后等到下一次 runloop 时再在合适的线程中执行 block。

在执行 block 之前,首先会寻找合适的线程来执行block,然后阻塞这个线程,直到 block 执行完毕。寻找线程的规则是: 任何提交到主队列的 block 都会在主线程中执行,在不违背此规则的前提下,文档还告诉我们系统会自动进行优化,尽可能的在当前线程执行 block。

顺便补充一句,GCD 死锁的充分条件是:“向当前队列重复同步提交 block”。从原理来看,死锁的原因是提交的 block 阻塞了队列,而队列阻塞后永远无法执行完 dispatch_sync(),可见这里完全和代码所在的线程无关。

另一个例子也可以证明这一点,在主线程中向一个串行队列同步的派发 block,根据上文选择线程的原则,block 将在主线程中执行,但同样不会导致死锁:

dispatch_queue_t queue = dispatch_queue_create("com.kt.deadlock", nil);
dispatch_sync(queue, ^{
    NSLog(@"current thread = %@", [NSThread currentThread]);
});
// 输出结果:
// current thread = {number = 1, name = main}
dispatch_queue_t queue = dispatch_queue_create("com.kt.deadlock", nil);
dispatch_sync(queue, ^{
    NSLog(@"current thread = %@", [NSThread currentThread]);
});
// 输出结果:
// current thread = {number = 1, name = main}

原因分析

啰嗦了这么多,回到之前描述的 bug 中来。现在我们知道,即使是在主线程中执行的代码,也很可能不是运行在主队列中(反之则必然)。如果我们在子队列中调用 MapKit 的 addOverlay 方法,即使当前处于主线程,也会导致 bug 的产生,因为这个方法的底层实现判断的是主队列而非主线程。

更进一步的思考,有时候为了保证 UI 操作在主线程运行,如果有一个函数可以用来创建新的 UILabel,为了确保线程安全,代码可能是这样:

- (UILabel *)labelWithText: (NSString *)text {
    __block UILabel *theLabel;
    if ([NSThread isMainThread]) {
        theLabel = [[UILabel alloc] init];
        [theLabel setText:text];
    }
    else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            theLabel = [[UILabel alloc] init];
            [theLabel setText:text];
        });
    }
    return theLabel;
}
- (UILabel *)labelWithText: (NSString *)text {
    __block UILabel *theLabel;
    if ([NSThread isMainThread]) {
        theLabel = [[UILabel alloc] init];
        [theLabel setText:text];
    }
    else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            theLabel = [[UILabel alloc] init];
            [theLabel setText:text];
        });
    }
    return theLabel;
}

从严格意义上来讲,这样的写法不是 100% 安全的,因为我们无法得知相关的系统方法是否存在上述 Bug。

解决方案

由于提交到主队列的 block 一定在主线程运行,并且在 GCD 中线程切换通常都是由指定某个队列引起的,我们可以做一个更加严格的判断,即用判断是否处于主队列来代替是否处于主线程。

GCD 没有提供 API 来进行相应的判断,但我们可以另辟蹊径,利用 dispatch_queue_set_specific 和 dispatch_get_specific 这一组方法为主队列打上标记:

+ (BOOL)isMainQueue {
    static const void* mainQueueKey = @"mainQueue";
    static void* mainQueueContext = @"mainQueue";
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil);
    });
    return dispatch_get_specific(mainQueueKey) == mainQueueContext;
}
+ (BOOL)isMainQueue {
    static const void* mainQueueKey = @"mainQueue";
    static void* mainQueueContext = @"mainQueue";
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil);
    });
    return dispatch_get_specific(mainQueueKey) == mainQueueContext;
}

用 isMainQueue 方法代替 [NSThread isMainThread] 即可获得更好的安全性。

参考资料

Community bug reports about MapKit

GCD’s Main Queue vs Main Thread

ReactiveCocoa 中遇到类似的坑

Why can’t we use a dispatch_sync on the current queue?

主线程中也不绝对安全的 UI 操作的更多相关文章

  1. Linux 下子线程 exit code 在主线程中的使用

    Linux线程函数原型是这样的: void* thread_fun(void* arg) 它的返回值是 空类型指针,入口参数也是 空类型指针.那么线程的 exit code 也应该是 void * 类 ...

  2. [原]unity中WWW isDone方法只能在主线程中调用

    项目中要使用动态加载,原计划是生成WWW对象后,放到一个容器里.由一个独立线程轮询容器里的对象,如果www.isDone为true时,回调一个接口把结果交给请求方. new Thread( new T ...

  3. 用Handler的post()方法来传递线程中的代码段到主线程中执行

    自定义的线程中是不能更新UI的,但是如果遇到更新UI的事情,我们可以用handler的post()方法来将更新UI的方法体,直接传送到主线程中,这样就能直接更新UI了.Handler的post()方法 ...

  4. android4.0以上访问网络不能在主线程中进行以及在线程中操作UI的解决方法

    MONO 调用一个线程操作UI 然后报Only the original thread that created a view hierarchy can touch its views.错误 goo ...

  5. 在主线程中慎用WaitForSingleObject (WaitForMultipleObjects)

    下面的代码我调试了将近一个星期,你能够看出什么地方出了问题吗?线程函数: DWORD WINAPI ThreadProc(    while(!bTerminate)    {        // 从 ...

  6. Android中,子线程使用主线程中的组件出现问题的解决方法

    Android中,主线程中的组件,不能被子线程调用,否则就会出现异常. 这里所使用的方法就是利用Handler类中的Callback(),接受线程中的Message类发来的消息,然后把所要在线程中执行 ...

  7. httpUrlConnection连接网络的用法(用到了handle传递消息,在主线程中更新UI)

    由于httpclient在Android5.0以后已经过时,所以官方推荐使用httpUrlConnection来连接网络,现将该连接的基本方法展示,如下 注意:记得加入<uses-permiss ...

  8. 在非主线程中更新UI

    在非主线程中调用了showMessage方法,结果报错:Can't create handler inside thread that has not called Looper.prepare() ...

  9. Java线程和多线程(四)——主线程中的异常

    作为Java的开发者,在运行程序的时候会碰到主线程抛异常的情况.如果开发者使用Java的IDE比如Eclipse或者Intellij IDEA的话,可能是不需要直接面对这个问提的,因为IDE会处理运行 ...

随机推荐

  1. FJUT寒假作业涨姿势题解

    题意非常简单易懂,对于涨姿势0,数据非常小,比较容易想到的是直接循环暴力解题完成任务.把数据放入数组arr,循环i,j控制所有区间算和.结果记入vis. 到了涨姿势1,2,3,我们观察数据变化,发现数 ...

  2. MongoDB Java

    MongoDB Java 环境配置 在Java程序中如果要使用MongoDB,你需要确保已经安装了Java环境及MongoDB JDBC 驱动. 你可以参考本站的Java教程来安装Java程序.现在让 ...

  3. 2017-暑假作业-Java语言程序设计

    任务列表 1.学会使用Markdown做笔记 本篇随笔就是使用的Markdown语法.养成做笔记的习惯! 参考资料: 极简MarkDown排版介绍(How to) stackedit:在线Markdo ...

  4. Dynamics CRM2016 Web API之删除单个查找字段值

    之前的博文中有介绍过,Web Api中的一个删除单个属性的Api但没提供查找字段的删除方法,本篇补充上,这里给出的示例代码是C#的(主要看url的拼接),看下url中最后的/$ref,这个标示表明了当 ...

  5. java异常拾遗

    概述 当方法内部发生一项错误时,该方法会创建一个对象传递给运行时系统(runtime system),这个对象被称为异常对象,包含错误的类型.发生位置,程序状态等一系列信息. 当一个方法抛出异常时,运 ...

  6. python模块:网络协议和支持

    python模块:网络协议和支持 webbrowser 调用浏览器显示html文件 webbrowser.open('map.html') [webbrowser - Convenient Web-b ...

  7. Android6.0 init 深入分析

    之前写过一篇关于android5.0 init的介绍,这篇博客是介绍android6.0init,之前有的代码介绍不详细.而且分析 解析init.rc那块代码也没有结合init.rc介绍. 一. ma ...

  8. Android Multimedia框架总结(十五)Camera框架之Camera2补充

    转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52751055 前言:监于5.0之 ...

  9. activiti 配置节点 连线信息获取

    1.1.1. 前言 当使用eclipse插件进行流程设计的时候,部署流程之后,我们如何获取我们定义的所有的节点.连线.关口等配置信息呢?有的人看到这个需求,不免窃喜,这不很简单嘛,重新打来bmpn中定 ...

  10. UIKit中ImageView动画堆叠显示的微调整

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 网上看到一个PackingList项目(如果需要源代码可以Q我 ...