iOS 多线程概览

iOS 多线程概览

二月 28, 2021
小时光

1、为什么所有的UI操作都在主线程中

不仅是iOS系统,包括Android等,所有的UI渲染、操作都在主线程中来完成。那为什么不采用多线程的方式呢?
使用多线程渲染UI更快,操作更流畅。但是系统设计者和开发者来说,需要解决线程问题的成本就更高了,也就是说成本远大于收益了。所以工程师们把所有的UI渲染和操作全都放在了主线程中。参考为什么必须在主线程操作UI

2、为什么要使用多线程

即然所有的UI操作都是单线程的,那么为何还需要多线程呢?在App开发中,所遇到不仅有UI操作,还有一些其他的费时操作,如网络请求、文件读取操作、AR模型下载等。此时就需要开发者把费时的操作放到子线程中去,完成后再返回住线程执行一些UI操作。

如上图所示,当进入这个Controller时开始下载图片,由于下载的图片较多需要几秒甚至十几秒,在下载时我同步滑动底部的UISlider,此时UISlider并没有滑动,直到所有的图片全部下载完成后才滑动。

试想如果我们的使用的每个App在费时操作时都需要等待很长时间用户才能操作,这样势必会给用户很不好的体验。此时的解决方案是创建一个新的线程来下载图片,这样既不影响用户的UI操作也不影响图片下载。

3、多线程的实现方式

Apple为开发者提供的三种多线程实现方式:

3.1、 Thread
  • 轻量级
  • 需要开发者手动管理线程生命周期和线程同步
3.2、 GCD(Grand Central Dispatch)
  • 相对Thread而言,不需要管理线程生命周期,操作更简单
  • 本身维护了一个线程池,会自动根据当前手机系统的情况来动态管理线程,不需要开发者来管理线程池和线程并发情况
  • 底层源码是开源的,点击Apple Open Spurce查看源码
3.3、 Cocoa Operation
  • 面向对象的API
  • 可以取消、依赖、任务优先级、可以子类化

4、多线程常用队列

多线程可以根据任务执行的队列方式分为三种队列:

  • 主队列: 在主线程中执行的任务
  • 串行队列(Serial Queue): 任务按照先后顺序执行,同一时刻只会执行一个任务
  • 并行队列(Concurrent Queue): 多个任务同时执行,完成的顺序不一定

参考 Apple Developer About Dispatch Queues

5、Serial Queue

  • (1)、串行队列处理并发任务

前面说过,对于一些耗时操作,一般将其放到一个子线程中执行,待完成后再返回主线程中刷新UI。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 图片下载管理类`DownloaderManager`
public class DownloaderManager: NSObject {
public class func downloadImageWithURL(_ url: String) -> UIImage? {
guard let data = try? Data(contentsOf: URL(string: url)!) else { return nil }
return UIImage(data: data)
}
}

// 创建一个队列
let serialQueue = DispatchQueue(label: "TSN.RPChat.io")
for (index, imgurl) in DownloaderManager.imageArray.enumerated() {
// 把下载任务加载到队列中
serialQueue.async {
let image = DownloaderManager.downloadImageWithURL(imgurl)
DispatchQueue.main.async {
girlsImg.image = image
}
}
}

此时再次运行代码,发现在下载图片的同时也能滑动UISlider,同时发现图片的加载顺序是按照从上到下的顺序加载的。默认情况下,系统会创建一个串行队列,也就是下载完成第一张图片之后再去下载第二张。此时对我来说,我的目的是下载并把图片全部显示出来,我并不关心图片的下载和加载顺序。这样我就需要在创建队列时设置这个队列为一个并行队列。

串行队列

6、Concurrent Queue

作为队列,concurrent queue中的任务虽然是按照进入队列的顺序启动,但不用等待之前的任务完成,iOS会根据当前系统情况启动多个线程并行执行队列中的任务。

在创建队列时设置attributes属性为concurrent就创建了一个并行队列。

1
let concurrentQueue = DispatchQueue(label: "TSN.RPChat.io", attributes: .concurrent)

此时运行工程,可以看到图片并不是按照先后顺序加载的,说明同一个concurrent queue中的所有任务在并行执行。
Concurrent Queue

7、面向对象的Cocoa Operation

上面创建的队列我使用的是GCD(Grand Central Dispatch)方式,尽管GCD对线程管理进行了封装并加入了面向对象管理模式。但是如果我要对一个队列中的任务做更多的操作,如(查看状态、取消任务,控制任务的执行顺序等)仍然不太方便。考虑到这些问题,苹果为开发这提供了一个面向对象方式的多任务执行机制OperationOperation是基于GCD的对象封装。

7.1、Operation概览

Operation的一些使用状态:

7.2、OperationQueue
  • OperationQueue可以加入多个Operation

    1
    2
    3
    4
    5
    6
    let ope1 = Operation()
    let ope2 = Operation()

    let que = OperationQueue()
    que.addOperation(ope1)
    que.addOperation(ope2)
  • maxConcurrentOperationCount可设置最大并发数当前,默认情况下,系统会根据当前情况动态确定最大并发数

1
que.maxConcurrentOperationCount = 5

此处需要注意的是最大并发数并不是线程数,最大并发数表示的是当前队列最多可同时执行的的任务(或线程)数量。

  • 可取消所有Operation,但当前正在执行的Operation不会取消
  • 所有的Operation执行完毕后退出销毁
7.3、BlockOperation
1
2
3
4
5
6
let queblock = BlockOperation.init(block: { [weak self] in

})
let que = OperationQueue.init()
que.maxConcurrentOperationCount = 3
que.addOperation(queblock)
7.4、completionBlock

当执行完一个任务时的回调.
我们可以通过创建Operation的方法,首先创建一个Operation对象,然后将其添加到队列中,这样做就可以通过设置completionBlock,在任务完成时得到通知。

7.5、默认优先级

苹果为Operation提供了优先级,Operation通过qualityOfService属性来控制其优先级,来看源码:

1
2
3
4
5
6
7
8
9
10
11
12
public enum QualityOfService : Int {

case userInteractive = 33

case userInitiated = 25

case utility = 17

case background = 9

case `default` = -1
}
  • userInteractive: 最高优先级,用于用户交互事件
  • userInitiated:次高优先级,用于用户需要马上执行的事件
  • utility:普通优先级,用于普通任务
  • background:最低优先级,用于不重要的任务
  • default:默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
1
operation.qualityOfService = .default

通过queuePriority属性来控制在OperationQueue中的优先级:

1
2
3
4
5
6
7
8
9
10
11
12
public enum QueuePriority : Int {

case veryLow = -8

case low = -4

case normal = 0

case high = 4

case veryHigh = 8
}
1
operation.queuePriority = .high
7.6、使用OperationQueue下载图片
1
2
3
4
5
6
7
8
9
10
11
// 创建一个队列
let queue = OperationQueue()

// 创建一个Operation
queue.addOperation {
et image = DownloaderManager.downloadImageWithURL(imgurl)
OperationQueue.main.addOperation {
girlsImg.image = image
}
}

这里需要注意的是:更新UI的代码要放到主线程中完成。使用OperationQueue.main获取主线程队列,然后添加addOperation把更新UI的任务放到主线程。
如果需要在下载完成时做一些相关操作,可以使用completionBlock,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let operation = BlockOperation(block: {
// 要执行的任务,如下载图片等
let image = DownloaderManager.downloadImageWithURL(imgurl)
// 下载完成后,返回主线程渲染图片
OperationQueue.main.addOperation {
girlsImg.image = image
}
})
operation.completionBlock = {
// 执行完成后
}
// 设置最大并发数 不设置时系统回根据当前情况动态设置最大并发数 设置为1时为串行队列
queue.maxConcurrentOperationCount = 5
// 将Operation添加到queue队列中
queue.addOperation(operation)
7.7、设置任务之间的关联性

设置任务之间的关联性
如图所示,当点击download按钮的时候开始下载图片,但是客户要求按照432的顺序加载,但是图片1不影响,此处需要用到addDependency方法,让图片按照432的顺序下载,图片1并行下载,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
let queue = OperationQueue()

let operation1 = BlockOperation(block: {
let image = DownloaderManager.downloadImageWithURL(imgArray[0])
OperationQueue.main.addOperation {
self.girlsImg1.image = image
}
})
operation1.completionBlock = {
print("-------operation1")
}

let operation2 = BlockOperation(block: {
let image = DownloaderManager.downloadImageWithURL(imgArray[1])
OperationQueue.main.addOperation {
self.girlsImg2.image = image
}
})
operation2.completionBlock = {
print("-------operation2")
}

let operation3 = BlockOperation(block: {
let image = DownloaderManager.downloadImageWithURL(imgArray[2])
OperationQueue.main.addOperation {
self.girlsImg3.image = image
}
})
operation3.completionBlock = {
print("-------operation3")
}

let operation4 = BlockOperation(block: {
let image = DownloaderManager.downloadImageWithURL(imgArray[7])
OperationQueue.main.addOperation {
self.girlsImg4.image = image
}
})
operation4.completionBlock = {
print("-------operation4")
}

operation3.addDependency(operation4)
operation2.addDependency(operation3)

queue.addOperation(operation1)
queue.addOperation(operation4)
queue.addOperation(operation3)
queue.addOperation(operation2)

此处把添加OperationQueue的操作,放到了addDependency之后,确保执行前有正确的依赖关系。多次运行代码可以看到,图片的下载顺序依然是4->3->2的顺序。

1
2
3
4
-------operation1
-------operation4
-------operation3
-------operation2
7.8、取消执行的任务

除了设置一个队列中任务关联性之外,还可以控制取消队列中的任务,但是取消的结果会根据任务的状态而不同:

  • 已经完成的任务,取消不影响其结果

  • 当一个任务被取消时所有与其关联的任务也会被取消

  • 任务被取消后,completionBlock依旧会执行

    取消图片下载

    如图所示,当我点击download按钮后快速点击cancel按钮,可以看到图片一的下载任务被cancel了。

    1
    2
    3
    4
    5
    let cancelItem = UIBarButtonItem()
    cancelItem.title = "cancel"
    cancelItem.rx.tap.subscribe(onNext: {
    self.queue.cancelAllOperations()
    }).disposed(by: disposeBag)

    此处可以通过OperationisCancelled属性来判断任务是否被cancel。当isCancelled返回true表示该任务被cancel了。

1
2
3
4
-------operation4,false
-------operation3,false
-------operation2,false
-------operation1,true

本文主要简单介绍了Operation的一些简单应用,正确的理解和应用这些多线程技术是构建复杂App的基础,关于更多多线程的应用可参考官方多线程的文档:

Apple Developer Threading Programming Guide

Apple Developer About Dispatch Queues

本文demo