本篇文章介绍的是在Swift3语言下的GCD应用操作,针对的目标读者可以是没有GCD基础的初学者,也可以是对GCD有一定的了解但想更加全面的了解开发者。
一、GCD 介绍
- 背景
在 iOS 当中,苹果提供了两种方式进行多任务编程:Grand Central Dispatch (GCD) 和 NSOperationQueue。当我们需要把任务分配到不同的线程中,或者是非主队列的其它队列中时,这两种方法都可以很好地满足需求。选择哪一种方法是很主观的行为,但是本教程只关注前一种,即 GCD。不管使用哪一种方法,有一条规则必须要牢记:任何操作都不能堵塞主线程,必须使其用于界面响应以及用户交互。所有的耗时操作或者对 CPU 需求大的任务都要在并发或者后台队列中执行。
GCD 是在 iOS 4 中推出的,它为并发、性能以及并行任务提供了很大的灵活性和选择性。但是在 Swift 3 之前,它有一个很大的劣势:由于它的编程风格很接近底层的 C,与 Swift 的编程风格差别很大, API 很难记,即使是在 Objective-C 当中使用也很不方便。这就是很多开发都避免使用 GCD 而选择 NSOperationQueue 的主要原因。简单地百度一下,你就能了解 GCD 曾经的语法是怎么样的。
Swift 3 中,这些都有了很大的变化。Swift 3 采用了全新的 Swift 语法风格改写了 GCD,这让开发都可以很轻松地上手。而这些变化让我有了动力来写这篇文章,这里主要介绍了 Swift 3 当中 GCD 最基础也最重要的知识。如果你曾经使用过旧语法风格的 GCD(即使只用过一点),那么这里介绍的新风格对你来说就是小菜一碟;如果你之前没有使用过 GCD,那你就即将开启一段编程的新篇章。
-
GCD 概念
- 队列
首先,GCD 中的核心词是 dispatch queue。一个队列实际上就是一系列的代码块,这些代码可以在主线程或后台线程中以同步或者异步的方式执行。一旦队列创建完成,操作系统就接管了这个队列,并将其分配到任意一个核心中进行处理。不管有多少个队列,它们都能被系统正确地管理,这些都不需要开发者进行手动管理。队列遵循 FIFO 模式(先进先出),这意味着先进队列的任务会先被执行(想像在柜台前排队的队伍,排在第一个的会首先被服务,排在最后的就会最后被服务)。
- 任务
接下来,另一个重要的概念就是 WorkItem(任务项)。一个任务项就是一个代码块,它可以随同队列的创建一起被创建,也可以被封装起来,然后在之后的代码中进行复用。正如你所想,任务项的代码就是 dispatch queue 将会执行的代码。队列中的任务项也是遵循 FIFO 模式。这些执行可以是同步的,也可以是异步的。对于同步的情况下,应用会一直堵塞当前线程,直到这段代码执行完成。而当异步执行的时候,应用先执行任务项,不等待执行结束,立即返回。我们会在后面的实例里看到它们的区别。
- 串行、并行
了解完上面两个概念(队列和任务项)之后,我们需要知道一个队列可以是串行或并行的。在串行队列中,一个任务项只有在前一个任务项完成后才能执行(除非它是第一个任务项),而在并行队列中,所有的任务项都可以并行执行。
在为主队列添加任务时,无论何时都要加倍小心。这个队列要随时用于界面响应以及用户交互。并且记住一点,所有与用户界面相关的更新都必须在主线程执行。如果你尝试在后台线程更新 UI,系统并不保证这个更新何时会发生,大多数情况下,这会都用户带来不好的体验。但是,所有发生在界面更新前的任务都可以在后台线程执行。举例来说,我们可以在从队列,或者后台队列中下载图片数据,然后在主线程中更新对应的 image view。
- 全局队列
我们不一定需要每次都创建自己的队列。系统维护的全局队列可以用来执行任何我们想执行的任务。至于队列在哪一个线程运行,iOS 维护了一个线程池,即一系列除主线程之外的线程,系统会从中挑选一至多条线程来使用(取决于你所创建的队列的数据,以及队列创建的方式)。哪一条线程会被使用,对于开发者来说是未知的,而是由系统根据当前的并发任务,处理器的负载等情况来进行“决定”。讲真,除了系统,谁又想去处理上述的这些工作呢。
-
初步认识
- 创建一个个队列
// 创建了一个标签是”queue0“、执行优先级为.default的并行队列 let queue0 = DispatchQueue(label: "queue0", qos: .default, attributes: .concurrent) // 属性解释: // label:队列标签 // qos:设置队列的优先级 // attributes:队列形式:默认串行,设置为.concurrent代表是并行队列 // 这个初始化方法中还有两个参数,但我们极少使用这里就不多介绍
- 创建一个简单的串行队列(不设置优先级)
let queue1 = DispatchQueue(label: "queue1")
- 创建一个有优先级的串行队列
let queue2 = DispatchQueue(label: "queue2", qos: .background)
- 创建一个并行队列
// 未设优先级 let queue3 = DispatchQueue(label: "queue3", attributes: .concurrent) // 设置了优先级 let queue4 = DispatchQueue(label: "queue4", qos: .background,attributes: .concurrent)
- 系统的全局队列
// 系统的全局队列 let queue5 = DispatchQueue.global() let queue6 = DispatchQueue.global(qos: .default)
- 系统的主队列
// 系统主队列:切记这主要用来操作UI页面相关信息,不要随便添加耗时任务进来 let main = DispatchQueue.main
GCD 串行
- 串行同步任务
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
queueExample()
}
func queueExample() {
// 串行队列
let queue1 = DispatchQueue.global()
// 同步任务
// 任务1
queue1.sync {
for i in 1...10 {
print("😁\(i)")
}
print("2:\(Thread.current)")
}
// 任务2
queue1.sync {
for i in 1000...1010 {
print("😎\(i)")
}
print("2:\(Thread.current)")
}
}
如下图效果,串行队列的同步任务,是按任务添加的顺序执行,而且不开辟新的线程,只在主线程操作,这样其实会造成主线程阻塞:
- 异步任务
// 串行队列
let queue1 = DispatchQueue.global()
// 异步任务
// 任务1
queue1.async {
for i in 1...10 {
print("😁\(i)")
print("1:\(Thread.current)")
}
}
// 任务2
queue1.async {
for i in 1000...1010 {
print("😎\(i)")
print("2:\(Thread.current)")
}
}
如下图效果,串行队列的异步任务,同时执行任务,而且是开辟了不同的线程进行操作的:
-
队列优先级
在使用 GCD 与 dispatch queue 时,我们经常需要告诉系统,应用程序中的哪些任务比较重要,需要更高的优先级去执行。当然,由于主队列总是用来处理 UI 以及界面的响应,所以在主线程执行的任务永远都有最高的优先级。不管在哪种情况下,只要告诉系统必要的信息,iOS 就会根据你的需求安排好队列的优先级以及它们所需要的资源(比如说所需的 CPU 执行时间)。虽然所有的任务最终都会完成,但是,重要的区别在于哪些任务更快完成,哪些任务完成得更晚。
用于指定任务重要程度以及优先级的信息,在 GCD 中被称为 Quality of Service(QoS)。事实上,QoS 是有几个特定值的枚举类型,我们可以根据需要的优先级,使用合适的 QoS 值来初始化队列。如果没有指定 QoS,则队列会使用默认优先级进行初始化。要详细了解 QoS 可用的值,可以参考这个文档,请确保你仔细看过这个文档。下面的列表总结了 Qos 可用的值,它们也被称为 QoS classes。第一个 class 代码了最高的优先级,最后一个代表了最低的优先级:
.background < .utility < .unspecified < .deault < .userInitiated < .userInteractive
GCD 并行队列
并行队列的创建是在初始化方法中设置attributes:.concurrent
这一属性实现
- 并行-同步任务
// 并行队列
let queue1 = DispatchQueue(label: "queue1", attributes: .concurrent)
// 任务1
queue1.sync {
for i in 1...10 {
print("😁\(i)")
}
print("1:\(Thread.current)")
}
// 任务2
queue1.sync {
for i in 1000...1010 {
print("😎\(i)")
}
print("2:\(Thread.current)")
}
如下图效果,并行队列的同步任务,是按任务添加的顺序执行,而且不开辟新的线程,只在主线程操作,这样其实会造成主线程阻塞:
- 异步任务
// 同步队列
let queue1 = DispatchQueue(label: "queue1", attributes: .concurrent)
// 任务1
queue1.async {
for i in 1...10 {
print("😁\(i)")
}
print("1:\(Thread.current)")
}
// 任务2
queue1.async {
for i in 1000...1010 {
print("😎\(i)")
}
print("2:\(Thread.current)")
}
// 任务3
queue1.async {
for i in 100000...100010 {
print("😑\(i)")
}
print("3:\(Thread.current)")
}
// 任务4
queue1.async {
for i in 1000000000...1000000010 {
print("👌\(i)")
}
print("4:\(Thread.current)")
}
如下图效果,并行队列的异步任务,同时执行任务,而且是开辟了不同的线程进行操作的:
GCD 延时操作
有时候,程序需要对代码块里面的任务项进行延时操作。GCD 允许开发者通过调用一个方法来指定某个任务在延迟特定的时间后再执行。
下面将用最简单的延迟方法进行演示:
let queue1 = DispatchQueue(label: "queue1")
print(Date())
// 让其延迟2秒操作
queue1.asyncAfter(deadline: .now() + 2) {
print(Date())
}
如下图效果,延迟了两分钟操作
GCD 分步操作、线程通信
我们经常也会有这样一种需要,一个任务必须在另外一个任务完成后再进行操作,又或者在其他线程中完成任务后,再回到主线程中去刷新UI页面。GCD中有两个类可以实现:DispathGroup
和DispathWorkItem
- DispathGroup
// 创建队列组
let group = DispatchGroup()
// 队列1
let queue1 = DispatchQueue(label: "queue1")
// 给队列添加任务并放到队列组中
queue1.async(group: group) {
for i in 1...10 {
print("😁\(i)")
}
print("1:\(Thread.current)")
}
// 队列2
let queue2 = DispatchQueue(label:"queue2")
// 在group中的任务执行完后会通告这里面的任务进行
group.notify(queue: queue2) {
// 回到对列2中进行操作
queue2.async {
for i in 1000...1010 {
print("😎\(i)")
}
print("2:\(Thread.current)")
}
}
如下图效果,队列2中的任务确实是在队列1的任务完成后才执行的:
- DispathWorkIten
// 队列1
let queue1 = DispatchQueue(label: "queue1")
// 任务1
let work1 = DispatchWorkItem {
for i in 1...10 {
print("😁\(i)")
}
print("1:\(Thread.current)")
}
// 任务2
let work2 = DispatchWorkItem {
for i in 1000...1010 {
print("😎\(i)")
}
print("2:\(Thread.current)")
}
// 第一种绑定方式
work1.notify(queue: queue1) {
// 执行2
work2.perform()
}
// 第二中绑定方式
work1.notify(queue: queue1, execute: work2)
// 第三种可以直接不创建work2,在闭包中执行work的工作
work1.notify(queue: queue1) {
for i in 1000...1010 {
print("😎\(i)")
}
print("2:\(Thread.current)")
}
// 执行任务:
// 方式1:任务放在队列中并执行
queue1.async(execute: work1)
// 方式2:如果不指定队列,会在当前的队列中执行,如果在主线程中执行会造成线程阻塞
work1.perform()
如下图效果,任务2确实是在任务1的完成后才执行的:
其他的一些细小的点,大家可以到官方文档查看,自己测试实验。