从最初开始学习 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. VS2010 win7 64位安装后新建项目生成时错误:LINK : fatal error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏

    解决方案:VS2010在经历一些更新后,建立Win32 Console Project时会出“error LNK1123” 错误,解决方案为将 项目|项目属性|配置属性|清单工具|输入和输出|嵌入清单 ...

  2. Linux(十七)动态监控进程

    17.1 介绍 top与ps命令很相似.它们都用来显示正在执行的进程.top与ps最大的不同之处,在于top在执行一段时间可以更新正在运行的进程 17.2 语法 top    [选项] 常用选项   ...

  3. Bootstrap3 栅格系统-栅格参数

    通过下表可以详细查看 Bootstrap 的栅格系统是如何在多种屏幕设备上工作的. -–下面有个"顶"字,你懂得O(∩_∩)O哈哈~ -–乐于分享,共同进步! -–更多文章请看:h ...

  4. 计算机网络之远程终端协议TELNET

    TELNET 是一个简单的远程终端协议.用户用 TELNET 就可在其所在地通过 TCP 连接注册(即登录)到远地的另一个主机上(使用主机名或 IP 地址). TELNET 能将用户的击键传到远地主机 ...

  5. The Zen Programmer

    专注 何为专注 关于 休息 怎么睡觉 心无杂念 我的体会 自我分析 初学者心态 无我 不要设置职业目标 敏事慎言 正念 做自己的老板 玩物养志 结语 最近在研读Christian Grobmeier ...

  6. 数组中的数分为两组,让给出一个算法,使得两个组的和的差的绝对值最小,数组中的数的取值范围是0<x<100,元素个数也是大于0, 小于100 。

    比如a[]={2,4,5,6,7},得出的两组数{2,4,6}和{5,7},abs(sum(a1)-sum(a2))=0: 比如{2,5,6,10},abs(sum(2,10)-sum(5,6))=1 ...

  7. 【安卓开发】Android为什么选择binder

    Binder (Android技术内幕): 在上面这些可供选择的方式中,Android使用得最多也最被认可的还是Binder机制. 为什么会选择Binder来作为进程之间的通信机制呢?因为Binder ...

  8. Dynamics CRM Plugin DLL恢复工具

    本篇接上篇继续介绍XrmToolBox中的某项功能,该工具的下载见上篇博文. 打开XrmToolBox,找到Assembly Recovery Tool,点击进去 如何连接上你的组织继续不表,列表中显 ...

  9. K均值聚类的失效性分析

    K均值聚类是一种应用广泛的聚类技术,特别是它不依赖于任何对数据所做的假设,比如说,给定一个数据集合及对应的类数目,就可以运用K均值方法,通过最小化均方误差,来进行聚类分析. 因此,K均值实际上是一个最 ...

  10. [virtualenv]生产环境中使用virtualenv

    virtualenv 对于python开发和部署都是好工具,可以隔离多个python版本和第三方库的版本,这里作者总结了几个常用python服务怎么样结合virtual部署 原文链接 Python 中 ...