引言
关于iOS
开发中的多线程,一直是工作中的重要组成部分。由于难以理解且对app
的用户体验影响重大,也是面试中的考察重点,一名合格的iOS
程序🐒理应掌握多线程编程。
自己虽然以前有学习过关于iOS
多线程的部分,但由于当时对iOS
开发还处于懵懂阶段,很多地方理解可能均有问题,遂查阅一些资料重温了GCD
的相关内容,并撰写此文已记录自己的学习路程。
ps:本文将分为三部分,以下内容是关于GCD
的基本概念及常见API
。
为了更好的阅读体验,推荐到我的博客阅读。
此文的第二部分👇
iOS中的多线程编程:重温GCD(二)
码字不易,各位看官看的喜欢烦请点个赞吧!以示鼓励啊😢
什么是GCD
🍎Crand Central Dispatch Reference👇:
Grand Central Dispatch (GCD) comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code executikkon on multicore hardware in iOS and OS X.
重温GCD术语
任务与队列 Task & Queue
GCD
术语中有两个核心的概念:任务,队列。
在本文中,我们可以将任务暂定为objc
中的一个block
。我们可以把任务与队列都看成是objc
中的对象。任务与队列都有他们自己的属性与行为。
任务的属性是:同步or异步
队列的属性是:并发or串行
串行与并发 | Serial & Concurrent
串行与并发是队列派发任务的行为描述,可以理解为队列的属性。既然是队列,那么必定遵循FIFO
的原则。无论是串行还是并发队列,一定都是先进先出即先进入队列的一定会被先执行,但是不一定先完成。
串行:
串行含义就是一个接一个的派发任务,注意理解一个接一个,即为要等待上一个任务完成了,才会派发下一个任务。这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD
一次只执行一个任务,并且按照我们添加到队列的顺序来执行。
虽然串行队列只有在上一个任务执行完毕了才会派发下一个任务,但这并不意味着下一个任务就会被立即执行。注意理解执行与派发,执行是GCD
来做的,而派发是由队列来做的。
并行:
并行的意义就与串行相反,虽然它也会按照FIFO
原则派发任务,但是它并不会等待上一个任务完成后才会派发下一个任务,而是直接将任务抛出。
就像图中那样,四个任务都会按照你添加的顺序去执行,and that’s about all you’re guaranteed!与串行队列同样,任务何时去执行,同时有几个任务去执行等都完全取决于GCD
。
ps:注意此处是==并发==不是==并行==,二者不可混淆。若有兴趣,可参考👇:
所谓的“并发”,英文翻译是concurrent。而并行是parallelism。
并发指的是一种现象,一种经常出现,无可避免的现象。它描述的是“多个任务同时发生,需要被处理”这一现象。它的侧重点在于“发生”。
比如有很多人排队等待检票,这一现象就可以理解为并发。
并行指的是一种技术,一个同时处理多个任务的技术。它描述了一种能够同时处理多个任务的能力,侧重点在于“运行”。
比如景点开放了多个检票窗口,同一时间内能服务多个游客。这种情况可以理解为并行。
常见的队列们:
首先,系统提供给你一个叫做 主队列(main queue
) 的特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发送消息给 UIView
或发送通知的。
系统同时提供给你好几个并发队列。它们叫做 全局调度队列(Global Dispatch Queues
) 。目前的四个全局队列有着不同的优先级:background、low、default 以及 high。要知道,Apple
的 API
也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。
最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。
以上是调度队列的大框架!
同步与异步 | Synchronous & Asynchronous
上文提到同步与异步都是任务对象的“属性”,这个属性的作用就是标志着任务被执行的方式:
同步
同步的含义是该任务执行时,需要等这个任务完成了,才继续线程中的下一个任务。如果你对多线程有一定的了解,你能立即领悟到同步肯定是在当前线程中执行任务的,而需要等待这个任务完成的特性使得同步任务必然会阻塞当前线程。
异步
与同步相反,异步任务并不需要被等待。即线程执行异步任务时,不会阻塞住当前线程。但是需要注意的是:执行异步任务时,并不一定会开辟新的线程。原因是在主队列进行任务时,无论是异步还是同步,都将会被放在主线程中。
放个表格总结下:
example | 同步 | 异步 |
---|---|---|
主队列 | 主线程 | 主线程 |
串行队列 | 当前线程 | 开辟线程 |
并发队列 | 当前线程 | 开辟线程 |
注:一定要理解好“派发”与“执行”之间的关系,其实这个地方用对象来理解队列,线程,GCD
,串行or并行是很方便的。
上下文切换 Context Switch
一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
线程安全 Thread Safe
线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary
。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDict;ionary
就不是线程安全的,应该保证一次只能有一个线程访问它。
小心!线程死锁
线程死锁是使用GCD
使用不当时的常见问题,尤其是主线程中,需要格外小心。造成线程死锁的原因是同步执行某个任务A,但是这个任务又在串行队列的后方。此时GCD
要求任务A立即执行,即阻塞住线程。可队列是要求遵循先进先出的原则,于是线程死锁。
举个栗子🌰:(这地方之前理解有问题,已经重新写了,感谢评论区🍊桔子同学指出)
//同步串行队列
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{//这句的作用是:将block里的内容作为任务添加到当前队列里,并且等待此任务执行结束
NSLog(@"2");
});
NSLog(@"3");
这样使用是会导致线程死锁的,控制台只会输出“1”。这个地方一定要理解好,当程序开始运行时,从第3行开始,到第11行都应该看做一个任务,即此时当前串行队列里应该是只有一个任务的。当程序执行到第5行时,当前这个任务还没有执行完毕!还在队列里!此时又将NSLog(@"2");
加入到了当前队列里,此时队列里有了两个任务!,即从3到11行是一个任务,block里又是一个任务。如下图:
//程序开始执行时...
<—————————— 出队顺序
任务1 (从第3行到第11行)
//程序执行到第5行...
<—————————— 出队顺序
任务1 (从第3行到第11行) ---- 任务2 (这个是执行到第5行时添加的任务)
由于任务1卡在任务2前,导致2任务无法被执行。但由于2任务是同步任务,必须先被执行,于是1等2,2等1造成线程死锁。
这样就解释了🍊桔子同学的疑惑,甚至这样写也会导致线程死锁,这就验证了我们上面的结论:
- (void)viewDidLoad{
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"123");
});
}
控制台是不会输出123的,理由同上,此时dispatch_sync
任务无法执行完毕。
从API
到使用场景
讲完了基本概念,就可以开始code
啦~
用 dispatch_async
处理后台任务:
在讲解dispatch_async
之前,先来看看这样的一个🌰:
假设你有一个vc
用于展示一些从网络上加载的图片,于是再没有学习GCD
之前你可能会这样处理:
- (void)viewDidLoad{
[super viewDidLoad];
//a long time work like download a img from network..
[self ALongTimeWork];
//some ui code here..
[self initUI];
}
是的,这看上去合乎情理:在viewDidLoad
方法里我们先去拉取网络数据,然后更新UI界面。但是通过上面的学习你可以了解到:
拉取网络数据是在主线程中同步进行的,这会阻塞住主线程。这就意味着在[self ALongTimeWork]
方法结束前,不仅[self initUI]
不会调用,连viewDidLoad
也会无法结束。这就会引起这个vc
加载的非常慢,带来的用户体验就是“这个app很卡!”这样的糟糕印象。
于是我们可以通过dispath_async
这个函数去开辟一个新的线程,并将长时任务放置全局并发队列里去执行:
- (void)viewDidLoad{
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//a long time work like download a img from network..
[self ALongTimeWork];
dispatch_sync(dispatch_get_main_queue(), ^{
//some ui code here..
[self initUI];
});
});
}
运行程序,你会发现vc
会直接加载完毕,过一会儿图片也会出来。在此期间用户也可以直接操作你的app
。这样的体验显然是完胜上面的方案的,这就是GCD
的价值!
使用 dispatch_after
延迟处理:
dispatch_after
函数可能很不起眼,但是在日常开发中却会经常使用到它。比如以下这种情况:
假设你需要在UITextView
中插入一段link
并用蓝色标注它们,告诉用户这个地方是可以点击的。显然使用attributedText
是个不错的选择。我们可以在用户点击link
时更换这段attributedText
的背景色为灰色并且跳转,就像大多数网站做的那样:
self.msgDetalTextView.attributedText =
[self changeNSMutableStringWithStr:jumpStr]; //改变attributedText背景色
NSURL *url = [NSURL URLWithString:_model.messageScheme];
if ([url scheme]) { //跳转动作
[LocalSchemeHandler performDefaultLocalJump:[url absoluteString]];
}
显然这样是合乎情理的,但是随着使用你就会发现这样的问题:
改变背景色那一瞬间太短了,用户可能根本察觉不到就发生了跳转。这会给用户带来困惑:我点了什么东西导致的跳转?
如果我们能给用户一个良好的反馈表示用户点击了这个link
动作生效之后再跳转,就合乎情理多了。于是dispatch_after
派上了用场:
self.msgDetalTextView.attributedText =
[self changeNSMutableStringWithStr:jumpStr]; //改变attributedText背景色
NSURL *url = [NSURL URLWithString:_model.messageScheme];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //这个地方给个延迟反馈下用户的点击
if ([url scheme]) {
[LocalSchemeHandler performDefaultLocalJump:[url absoluteString]];
}
});
现在用户轻点下link
会导致link
区域变灰,给足0.2秒的时间告诉用户你的点击生效了!,之后再发生跳转。
需要注意的是:dispatch_after
并不会阻塞当前线程,就算参数中你选择的是dispatch_get_main_queue()
。这是因为dispatch_after
函数的含义并不是延迟执行,而是延迟提交。dispatch_after
的实质是异步将这个任务添加到一个串行队列里去,约定时间结束后,再从串行队列中取出任务添加到你参数中填的那个队列中。比如以下这个例子:
NSLog(@"let's go");
// 设置2秒后执行block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"This is my");
});
NSLog(@"let's go");
我们会在控制台看到两个“let's go”的输出,然后过两秒后才输出“This is my”。跟具体的可以看这篇文章。
用dispatch_once实现单例:
单例是iOS开发中
并不少见的设计模式,在不使用GCD
的情况下,我们可能会这样写:
+ (instancetype)shareASingleManager{
static ASingleManager *shareManager = nil;
if (!shareManager) {
shareManager = [[ASingleManager alloc] init];
//some init code here...
}
return shareManager;
}
在了解了线程的概念你就会发现这样其实是有问题的,if
语句可不是线程安全!
比如代码运行到第3行时,此时内存中并没有生成过shareManager
,于是进入if
条件中。但是由于线程切换,该任务暂时不再运行了。在新开辟的线程中,可能再次想要获取该单例,在之前的线程中虽然进入了if
,但还没来及生成shareManager
,于是造成了两个“单粒”的情况。这显然是不合乎情理的。
dispatch_once
函数即可解决这个问题,我们将上述代码改为:
+ (instancetype)shareASingleManager{
static ASingleManager *shareManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//这里只会来一次 直接生成对象即可
shareManager = [[ASingleManager alloc] init];
//some inti code here ...
});
return shareManager;
}
🍎爸爸的discussion
This function is useful for initialization of global data (singletons) in an application. Always call this function before using or testing any variables that are initialized by the block.
If called simultaneously from multiple threads, this function waits synchronously until the block has completed.
The predicate must point to a variable stored in global or static scope. The result of using a predicate with automatic or dynamic storage (including Objective-C instance variables) is undefined.
Executes a block object once and only once for the lifetime of an application.
The End
关于第二部分:
第二部分将介绍一些更深层次的GCD
的API
使用,例如dispatch_barrier_async
、dispatch_group_async
等。
此文的第二部分👇
iOS中的多线程编程:重温GCD(二)
码字不易,各位看官看的喜欢烦请点个赞吧!以示鼓励啊😢
参考:
Grand Central Dispatch In-Depth: Part 1/2