iOS 线程与队列之间的关系
引导问题:UI刷新,为什么需要在主线程中执行?
原因一:UIKit的操作不是线程安全的
在多个线程下进行UI操作,可能出现资源抢夺问题,出现问题,如下举出几个例子:
-
两个线程同时设置同一个背景图片,那么很有可能因为当前图片被释放了两次而导致应用崩溃。
-
两个线程同时操作view的树形结构:在线程A中for循环遍历并操作当前View的所有subView,然后此时线程B中将某个subView直接删除,这就导致了错乱还可能导致应用崩溃。
-
两个线程同时设置同一个UIView的背景颜色,那么很有可能渲染显示的是颜色A,而此时在UIView逻辑树上的背景颜色属性为B。导致应用view的颜色显示异常
原因二:iOS中UI刷新最终都要在主线程中执行。
-
先明确一点:子线程中可以修改UI。
注意:这里使用的词是修改。因为按照编程逻辑而言,代码无论在子线程还是主线程中,结果都是一样会被接纳的。所以,子线程中修改UI是没问题的。
但问题在于,UI元素被修改完之后,需要有刷新应用新修改结果的操作。但是子线程没有刷新UI(接受请求并运用)的权利,导致了我们在子线程中修改了UI而不变化,变成了提交了但未生效!
可以理解为:子线程中只是提交了一个修改请求,告诉计算机,我们要将此UI元素做变更!仅此而已
那谁有刷新UI的权利?主线程有!
主线程能进行修改UI元素,与子线程一致;并将修改立即生效的能力。此能力子线程不具备。
所以,如果子线中做修改UI的操作,但是其后如果出现了子线程长时间执行或等待导致无法回调到主线程执行刷新操作。那就会造成UI刷新失败的假象。
iOS4之后苹果将大部分绘图的方法和诸如 UIColor 和 UIFont 这样的类改写为了线程安全可用。
但是还是开发还是默认在主线程中刷新UI,以免出现一些异常情况
通过上面的问题,可以看出,刷新UI的任务,需要在主线程中执行。任务的调度者是队列queue,那queue与线程,有什么关系呢?
有一看的比较多的面试题:主线程与主队列的区别?
首先看看几个概念:
任务:
计算机工作的基本工作单元,它由控制程序处理的一个或者多个指令序列。
线程:
是 <u>操作系统</u> 能够进行运算调度的最小单位。它被包含在进程中,是进程中实际运作单位。
一条线程:指的是进程中某一单一顺序的控制流,
一个进程可以并发多个线程,每条线程执行不同的任务。
小总结:线程是任务实际运作单位。(任务需要被放入线程中,才能执行)
队列:
数据结构,拥有先进先出的特点。是计算机中,用于存放任务的基本单位,分为串行队列和并行队列(GCD)。
那第一个问题:线程与队列有什么关系呢?
按照个人的理解,两者之间的关系如下:
-
线程需要一个队列用于存放任务,否则任务一股脑的进入线程,将无法单一顺序执行
-
队列需要依托于一个线程来将其内部储存的任务消化掉。否则堆积了无数的任务却无法被执行。
在计算机中,两者的关系就好比是共生。我需要你,你也需要我。(举个例子:鸻与鳄鱼共生(生物之间的互利关系))
但是!(凡事都有但是的嘛)共生关系,并不存在“拥有关系“。
线程与队列之间的关系用一句谚语来形容更贴切:良禽择木而栖,贤臣择主而事。
- 队列(贤臣)需要找到一个线程(主)来将其储存的任务分发并执行出去。
此时,通过GCD sync与async的标识,队列对应的是一个线程呢,还是对应多个线程。 - 线程需要队列分发的任务来执行任务,完成进程所需的相关操作。
接下来,便要解决一开始的问题:主线程与主队列的区别?
我们再次先梳理下概念:
主线程:
当一程序启动时,就有一个进程被操作系统创建,与此同时一个线程也将被创建并立即运行。该线程即为进程的主线程
主线程的功能:
1. 用于创建其他的子线程
2. 通常它必须在最后完成执行(比如各种关闭操作)
主队列
一般情况下,当主线程被创建后,需要创建一个队列与之对应(否则线程找不到任务的来源),所以,与主线程一同被创建的队列我们称之为主队列。
如此一来。我们知道概念后,需要将他两应用。应用,也分情况讨论:
-
在单线程运作的情况下:
这种情况最好理解,单线程,只有主线程的存在,那只要是任务,都放入主队列中,让其分发给主线程执行即可。
简单吗?
有人会说,那我在单线程下,创建多个队列,让多个队列都对应上主线程,可以吗?
按照理论而言,个人认为这是完全没问题的。好比说好几个小孩都想玩公园里唯一一条滑滑梯,要么就头破血流般的抢着玩,要么就是你玩完到我的有顺序的玩。只需要滑滑梯可以被玩。(这次怎么怎么那么怪异呢?)
对应的主线程,只需要它能继续执行任务,不需要关心任务是否真正来自于主队列。
-
在多线程的情况下:
此种情况,就复杂了起来。因为进程不再是只有单一主线程,其有能力开启多条子线程,那是否有与子线程相对应的队列呢?那就不一定了。
我们目前只把关注点放在主队列和主线程上。
会有以下两个情况:
-
主线程不一定只执行主队列的任务
因为在多线程情况下,iOS做了优化,因为性能问题(切换线程是消耗性能的),所以在条件允许的情况下,即便是global队列的任务,也可能因线程停留在主线程而在主线程中执行。 -
主队列的任务不一定在主线程中执行。
-
注意🐖:在
iOS
中,可以认为是这样。因为是移动端吧,主线程消失,即标记为进程的结束。可能还是跟一开始移动硬件设备的硬件实力不足所遗留下来的问题。
在Mac OS
中,主线程可能会被提前释放,但是进程仍然存在,因为其进程中存在其他未被释放且长期驻存的线程。所以任务可以在这些子线程上执行。此时,主队列的认为找到了主线程,就可能被分配到其他的子线程上去执行了。
最后开始回答一开始的问题:区别!
两者能联系在一起,是因为他们都在对任务进行操作
所以区别很显然:队列与线程,就是两个不同的对象,其处理的业务逻辑本质就不一样。一个是执行任务,一个是存储并分发任务
针对主队列和主线程,就是OS赋予的特殊意义。在不同的OS上,对主线程的操作不完全相同,也根据进程的多线程复杂情况,导致两者在对应上,不一定完全。所以出现了上述的两种情况。
题外话:如何判断当前是否运行在主队列上呢?
为什么会有这样的问题?
问题描述:在苹果的MapKit框架中,有一个叫做addOverlay的方法,它在底层实现的时候,不仅仅要求代码执行在主线程上,还要求执行在 GCD 的主队列上。
按照前面的描述,我只判断当前是否为主线程,是不足以保证代码任务是从主队列上获取来的!故if([NSThread isMainThread])
这样的判断,是没用的!
此时,需要用到线程标记并判断来保证。
dispatch_queue_set_specific && dispatch_get_specific
RCT代码中截取片段:
代码作用:用于判断当前线程是否在主队列
+ (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;
}
dispatch_queue_set_specific
:用于给某一队列设置标记。
* 参数一:dispatch_queue_t queue
用来传入需要被标记的队列。
* 参数二:const void *key
标记的名字,就是个线程取别名
* 参数三:void *_Nullable context
别名的地址位置
* 参数四:dispatch_function_t _Nullable destructor
是个函数指针,一般只有上下文被忽略的时候才使用。一般为空
加上dispatch_once_t
的好处是在程序内此队列在程序内只会被标记一次,不会再有其他的名字。更好的用于判断
看代码不难理解:
dispatch_queue_set_specific
是用于给队列标记
dispatch_get_specific
则是获取标记的队列
接下来还有待总结的是:
- 在GCD下,async与sync 和 串行队列与并行队列 两两组合擦出的火花和可能造成死锁的情况
尽早总结更新吧~