美文网首页
主线程中也不绝对安全的 UI 操作无标题文章

主线程中也不绝对安全的 UI 操作无标题文章

作者: 不断上升中_ | 来源:发表于2016-08-02 10:10 被阅读16次

从最初开始学习 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_tqueue = dispatch_queue_create("com.kt.deadlock",nil);dispatch_sync(queue, ^{NSLog(@"current thread = %@", [NSThreadcurrentThread]);});// 输出结果:// current thread = {number = 1, name = main}

原因分析

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

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

- (UILabel*)labelWithText: (NSString*)text {    __blockUILabel*theLabel;if([NSThreadisMainThread]) {        theLabel = [[UILabelalloc] init];        [theLabel setText:text];    }else{dispatch_sync(dispatch_get_main_queue(), ^{            theLabel = [[UILabelalloc] init];            [theLabel setText:text];        });    }returntheLabel;}

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

解决方案

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

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

+ (BOOL)isMainQueue {staticconstvoid* mainQueueKey =@"mainQueue";staticvoid* mainQueueContext =@"mainQueue";staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{        dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext,nil);    });returndispatch_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 操作

    主线程中也不绝对安全的 UI 操作 主线程中也不绝对安全的 UI 操作

  • 主线程中也不绝对安全的 UI 操作无标题文章

    从最初开始学习 iOS 的时候,我们就被告知 UI 操作一定要放在主线程进行。这是因为 UIKit 的方法不是线程...

  • GCD

    GCD 队列与线程的关系 主队列和主线程 『ios』主线程 和 主队列的关系,绝对安全的UI操作,主线程中一定是主...

  • 『ios』线程死锁还是队列死锁? 死锁测试

    之前 『ios』主线程 和 主队列的关系,绝对安全的UI操作,主线程中一定是主队列?在这片文章中,总结了下主线程和...

  • Handler(Android消息处理机制)

    一、Why Handler?Android中的UI控件是非线程安全的,因此更新UI的操作只能放到UI线程中处理,也...

  • 主线程中也不绝对安全的 UI 操作

    从最初开始学习 iOS 的时候,我们就被告知 UI 操作一定要放在主线程进行。这是因为 UIKit 的方法不是线程...

  • Handler的使用

    Handler通信工作原理 Android中为了UI操作线程安全,只允许UI线程更新Activity里的UI组件。...

  • android线程管理总结

    一 前言: 在android中,UI主线程并非线程安全的,所有UI相关的操作均需在UI主线程中完成。在默认情况下,...

  • iOS 线程与队列之间的关系

    引导问题:UI刷新,为什么需要在主线程中执行? 原因一:UIKit的操作不是线程安全的 在多个线程下进行UI操作,...

  • AsyncTask和线程池

    在Android中,UI操作是线程不安全的,如果想要在子线程中进行UI操作,或者在主线程中进行耗时操作,则需要借助...

网友评论

      本文标题:主线程中也不绝对安全的 UI 操作无标题文章

      本文链接:https://www.haomeiwen.com/subject/qvndjttx.html