主线程中也不绝对安全的 UI 操作
从最初开始学习 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
Why can’t we use a dispatch_sync on the current queue?
主线程中也不绝对安全的 UI 操作的更多相关文章
- Linux 下子线程 exit code 在主线程中的使用
Linux线程函数原型是这样的: void* thread_fun(void* arg) 它的返回值是 空类型指针,入口参数也是 空类型指针.那么线程的 exit code 也应该是 void * 类 ...
- [原]unity中WWW isDone方法只能在主线程中调用
项目中要使用动态加载,原计划是生成WWW对象后,放到一个容器里.由一个独立线程轮询容器里的对象,如果www.isDone为true时,回调一个接口把结果交给请求方. new Thread( new T ...
- 用Handler的post()方法来传递线程中的代码段到主线程中执行
自定义的线程中是不能更新UI的,但是如果遇到更新UI的事情,我们可以用handler的post()方法来将更新UI的方法体,直接传送到主线程中,这样就能直接更新UI了.Handler的post()方法 ...
- android4.0以上访问网络不能在主线程中进行以及在线程中操作UI的解决方法
MONO 调用一个线程操作UI 然后报Only the original thread that created a view hierarchy can touch its views.错误 goo ...
- 在主线程中慎用WaitForSingleObject (WaitForMultipleObjects)
下面的代码我调试了将近一个星期,你能够看出什么地方出了问题吗?线程函数: DWORD WINAPI ThreadProc( while(!bTerminate) { // 从 ...
- Android中,子线程使用主线程中的组件出现问题的解决方法
Android中,主线程中的组件,不能被子线程调用,否则就会出现异常. 这里所使用的方法就是利用Handler类中的Callback(),接受线程中的Message类发来的消息,然后把所要在线程中执行 ...
- httpUrlConnection连接网络的用法(用到了handle传递消息,在主线程中更新UI)
由于httpclient在Android5.0以后已经过时,所以官方推荐使用httpUrlConnection来连接网络,现将该连接的基本方法展示,如下 注意:记得加入<uses-permiss ...
- 在非主线程中更新UI
在非主线程中调用了showMessage方法,结果报错:Can't create handler inside thread that has not called Looper.prepare() ...
- Java线程和多线程(四)——主线程中的异常
作为Java的开发者,在运行程序的时候会碰到主线程抛异常的情况.如果开发者使用Java的IDE比如Eclipse或者Intellij IDEA的话,可能是不需要直接面对这个问提的,因为IDE会处理运行 ...
随机推荐
- OAuth 2 深入介绍
1. 前言 2. OAuth2 角色 2.1 资源所有者(Resource Owner) 2.2 资源/授权服务器(Resource/Authorization Server) 2.3 客户端(Cli ...
- 【机器学习】从SVM到SVR
注:最近在工作中,高频率的接触到了SVM模型,而且还有使用SVM模型做回归的情况,即SVR.另外考虑到自己从第一次知道这个模型到现在也差不多两年时间了,从最开始的腾云驾雾到现在有了一点直观的认识,花费 ...
- 自定义支持多行显示的RadioGroup
自定义支持多行显示的RadioGroup 原生的RadioGroup继承自LinearLayout,即只能支持一横排或者一竖排的排列显示RadioButton 现在改写RadioGroup,使它支持多 ...
- Docker Volume 之权限管理(一)
摘要: Volume数据卷是Docker的一个重要概念.数据卷是可供一个或多个容器使用的特殊目录,可以为容器应用存储提供有价值的特性.然而Docker数据卷的权限管理经常是非常令人困惑的.本文将结合实 ...
- Android5.0特性阴影效果和裁剪
阴影和剪裁 View的z属性 Material Design建议为了凸显布局的层次,建议使用阴影效果,并且Android L为了简化大家的工作,对View进行了扩展,能使大家非常方便的创建阴影效果: ...
- 【SSH系列】Hibernate映射 -- 多对多关联映射
映射原理 在数据库学习阶段,我们知道,如果实体和实体之间的关系是多对多,那么我们就抽出来第三张表,第一张表和第二张表的主键作为第三表的联合主键,结合我们的hibernate,多对多关联,无论 ...
- 设子数组A[0:k]和A[k+1:N-1]已排好序(0≤K≤N-1)。试设计一个合并这2个子数组为排好序的数组A[0:N-1]的算法。
设子数组A[0:k]和A[k+1:N-1]已排好序(0≤K≤N-1).试设计一个合并这2个子数组为排好序的数组A[0:N-1]的算法.要求算法在最坏情况下所用的计算时间为O(N),只用到O(1)的辅助 ...
- EBS业务学习之采购管理
一.基础数据 w供应商档案 w采购员设置 w审批层次 w单据控制 w危险类代码 w检验代码 w自动来源 w供应商项目w目录册 二.业务流程 w请购单 w询价单 w报价单 w采购定单 w接收 w检验 w ...
- C++ ifstream,ofstream读写二进制文件
为什要吧数据存为二进制 这个嘛,是我个人习惯,一般,我们会把日志文件存为文本文件.数据文件存成二进制文件. 其实,我们接触的文件,比如图像.视频都是以二进制的形式存储的,要想查看这类数据,必须知道数据 ...
- SpriteKit物理引擎碰撞中5个重要信息
我们知道在SpriteKit物理引擎实际是基于Box2D! 在SpriteKit中当你设置好适当的碰撞参数后,通过遵守SKPhysicsContactDelegate,你可以选择实现2各碰撞回调方法: ...