-
Operation
使用 iOS多线程之 GCD 能够解决很多多线程的问题,但是你还可以使用 Operation 来处理多线程问题。那既然使用GCD也OK,什么时候用 Opeation 比较好呢?
- 需要监测任务的执行状态。
- 任务是封装好的,需要可复用。
- 任务需要在某些时候取消。
- 任务之间需要添加依赖等。
在上面的情况,如果使用 Operation 可能会比使用 GCD更方便处理多线程问题。Opeation 是基于 GCD 封装的,它能让你有更多的面向对象的思想来处理多线程。
在开发中,可以使用 Operation 执行任务,也可以把 Operation 添加到 OperationQueue,就和在 GCD 中把任务添加到 DispatchQueue(队列) 一样。
默认执行Operation任务是同步执行的,担任自己也可以添加一些设置让Operation在执行的时候是异步的,但是一般情况是把 Operation 添加到 OperationQueue 异步执行,这样更方便。
-
复用性
如果一个任务,只是简单的配置然后执行,那么使用 GCD 其实就能简单的处理了。但是如果这个任务是需要复用的,那么使用 Operation 会更好,因为 Operation 是一个类,是面向对象的。
-
Operation 的执行状态
由于 Operation 是一个类,你还可以查看 Operation 任务的执行状态,也可以看成是 Operation 的生命周期:
- isReady: 任务准备好了,可以执行了。
- isExecuting: 调用 Operation start 方法,此时任务是执行中。
- isCancelled: 调用 Operation cancel 方法,任务切换到取消状态(之后会自己切换到 isFinished 结束状态)。
- isFinished: 任务执行完毕。
这些状态属性都是 Operation 的 read-only 的 Bool 属性。Operation 会管理任务的执行状态,但是开发人员可以影响的状态是调用自己任务开始影响 isExecuting 和 调用 cancel 方法影响 isCancelled。
-
BlockOperation
由于 Operation 是一个抽象类,在实际使用的时候需要使用具体的类,而 BlockOperation 就是系统提供的 Operation 子类。这个类的实际操作其实很像是 GCD 的 closure。
比如:
let operation = BlockOperation {
print("1 + 1 = \(1+1)")
}
实现 BlockOperation 可以使用 KVO 的特性查看任务执行状态,或者 Operation 之间添加依赖。BlockOperation 在使用中比较类似 GCD 中的 DispatchGroup。 可以使用 operation.addExecutionBlock
添加多个执行Block, 当里面有多个执行Block的时候会默认添加到后台并发队列。
代码:
let operation = BlockOperation {
print("1 + 1 = \(1+1), Thread: \(Thread.current)")
}
operation.addExecutionBlock {
print("1 + 2 = \(1+2), Thread: \(Thread.current)")
}
operation.addExecutionBlock {
print("1 + 3 = \(1+3), Thread: \(Thread.current)")
}
operation.addExecutionBlock {
print("1 + 4 = \(1+4), Thread: \(Thread.current)")
}
operation.addExecutionBlock {
print("1 + 5 = \(1+5), Thread: \(Thread.current)")
}
operation.start()
输出:
1 + 1 = 2, Thread: <NSThread: 0x600003270ec0>{number = 1, name = main}
1 + 4 = 5, Thread: <NSThread: 0x60000321c2c0>{number = 3, name = (null)}
1 + 5 = 6, Thread: <NSThread: 0x600003212100>{number = 8, name = (null)}
1 + 2 = 3, Thread: <NSThread: 0x60000320d9c0>{number = 6, name = (null)}
1 + 3 = 4, Thread: <NSThread: 0x600003201000>{number = 7, name = (null)}
里面的所有任务是并发执行的,输出的顺序是不确定的。 第一个任务在主线程执行,其它任务在后台线程并发执行。
BlockOperation 的第一个block 任务会在主线程中执行。不管这个任务是 1: let operation = BlockOperation { // task } 的初始化任务,还是 2: let operation = BlockOperation(), operation.addExecutionBlock { // task } 的添加操作block的任务。
如果想要监听所有任务的完成状况,可以这样:
operation.completionBlock = {
print("All task done")
}
operation.start()
这个是和 DispatchGroup 一样的情况,任务都完成以后会执行异步完成方法 completionBlock
。
-
Operation 自定义子类
下面演示的是一个 TableView 的图片过滤功能
子类化Operation:
private let context = CIContext()
class TiltShiftOperation: Operation {
// 输出的图片
var outputImage: UIImage?
// 需要过滤的图片
private let inputImage: UIImage
init(image: UIImage) {
inputImage = image
super.init()
}
override func main() {
// 实际过滤方法
guard let filter = TiltShiftFilter(image: inputImage, radius: 3), let output = filter.outputImage else {
print("Failed to generate shift image")
return
}
let fromRect = CGRect(origin: .zero, size: inputImage.size)
guard let cgImage = context.createCGImage(output, from: fromRect) else {
print("No image generated")
return
}
outputImage = UIImage(cgImage: cgImage)
}
}
上面的 private let context = CIContext()
是线程安全的, CIContext 苹果建议是可以复用就复用,减少不必要的开销。main()
是具体的Operation执行方法。
TableView 里面cell显示的内容:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "normal", for: indexPath) as! PhotoCell
cell.display(image: nil)
let image = UIImage(named: "\(indexPath.item).png")!
// 演示目的,会阻塞主线程,但是把过滤方法抽离了
print("Filtering")
let op = TiltShiftOperation(image: image)
op.start() // sync
cell.display(image: op.outputImage)
print("Done")
return cell
}
上面演示的是把Cell图片的过滤功能抽离,交给 Operation 的子类 TiltShiftOperation 处理,上面的 op.start()
是一个同步方法,它会阻塞当前线程(也就是主线程),也就是说滚动 TableView 的时候会有卡顿的感觉。下面会介绍使用 OperationQueue 解决同步卡顿问题。
还有一个问题就是调用 `op.start()` 有时候如果当前 Operation 还没有确定是 `isReady` 状态会有异常(比如此时与 Operation 依赖)。但是由于是演示功能,所以就先这样。
-
OperationQueue
Operation 和 OperationQueue 结合使用才能更好的发挥多线程的能力。默认情况 Operation 执行是同步的,添加到 OperationQueue 以后就是异步执行了。添加任务到 OperationQueue 有三种方法:
-
添加一个 Operation。
-
添加closure。
-
添加多个 Operation。
-
OperationQueue 的管理
添加到 OperationQueue 里面的 Operation 只有是 isReady 状态才会执行。而一个 Operation 的 isReady 的状态受它的依赖或者 Qos 影响。当一个 Operation 添加到一个 OperationQueue 以后,那么它就不能添加到其它的 OperationQueue 了。
-
等待所有的 Operation 执行完成
默认情况下,OperationQueue 里面的 Operation 是异步并发执行的。如果想等待所有的 Operation 都执行完毕,那么可以这样:
let queue = OperationQueue()
queue.addOperation(op1)
queue.addOperation(op2)
queue.addOperation(op3)
queue.waitUntilAllOperationsAreFinished()
它会阻塞当前线程,直到所有的 Operation 执行完毕。如果是在是需要 waitUntilAllOperationsAreFinished
这个方法,那么可以在一个自定义的串行队列执行,那样主线程就是安全的了。
-
Quality of service (Qos)
OperationQueue 的默认 Qos 是 .background
,但是可以被添加进里面的Operation 影响,这个类似于 GCD 的任务添加到队列(毕竟 OperationQueue 是封装于 GCD 的)。
-
暂停 OperationQueue
调用 queue.isSuspended = true
,那么在 OperationQueue 里面的 Operation 如果是正在运行,那么没影响,如果是还没运行,那么这些还没运行的 Operation 会被挂起,直到设置 queue.isSuspended = false
才会执行。
-
Operation 的最多执行数目
默认情况下,添加到 OperationQueue 的 Operation 的最多并发执行数目是由系统根据资源使用情况确定的。如果想要设置 Operation 的最多同时执行数目,那么可以这样:
queue.maxConcurrentOperationCount = 1
。此时设置为 OperationQueue 里面一次只能执行一个 Operation,运行像一个串行队列一样。
-
解决上面TableView 滚动卡顿问题:
private let queue = OperationQueue()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "normal", for: indexPath) as! PhotoCell
cell.display(image: nil)
let image = UIImage(named: "\(indexPath.row).png")!
// 过滤 Operation
let op = TiltShiftOperation(image: image)
op.completionBlock = {
DispatchQueue.main.async {
// 因为 tableView 可能滚动,这样可以找到正确的cell
guard let cell = tableView.cellForRow(at: indexPath) as? PhotoCell else { return }
cell.isLoading = false
cell.display(image: op.outputImage)
}
}
// 添加到 OperationQueue
queue.addOperation(op)
return cell
}
把 TiltShiftOperation 添加到 OperationQueue 里面,OperationQueue 是默认异步执行的,不会阻塞主线程,此时滚动 TableView 就是顺畅的了。
-
异步 Operation
在上面介绍的 Operation 都是同步执行的。虽然把 Operation 添加到 OperationQueue 达到异步执行的效果,但是 Operation 里面的逻辑还是同步执行的。同步执行会是 Operation 调用 start()
再调用 main()
。main()
方法执行完毕 Operation 也就执行完毕了此时是 isFinished 状态。流程如下:
而异步执行的流程如下:
执行main()
方法里面的内容时会去执行异步方法,此时 main()
方法返回,但是 Operation 状态还不是 isFinished。那么什么一般什么情况需要初始化一个异步的 Operation 呢?一种使用场景是:网络下载图片。
-
一个异步 Operation 的封装
由于 Operation 类是一个抽象类,要使用一个异步 Operation,需要初始化它的子类。
-
AsyncOperation
下面是封装的异步执行类: AsyncOperation
(isCancelled 的状态会在下面的介绍中添加)
import Foundation
class AsyncOperation: Operation {
// 由于 Operation 的各个执行状态是 readOnly 只读的,所以添加这个状态辅助 enum
enum State: String {
case ready, executing, finished
fileprivate var keyPath: String {
return "is\(rawValue.capitalized)"
}
}
// Operation 执行状态的 KVO
var state = State.ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
didSet {
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
}
}
// 检查 super.isReady 很重要,因为这个需要知道具体的 Operation 是否已经准备好执行了。
override var isReady: Bool {
return super.isReady && state == .ready
}
override var isExecuting: Bool {
return state == .executing
}
override var isFinished: Bool {
return state == .finished
}
// 异步 Operation 设置
override var isAsynchronous: Bool {
return true
}
// 具体的执行方法,不要调用 super.start()
override func start() {
main() // main 方法直接返回
state = .executing
}
}
其实这个类可以作为异步 Operation 的父类,因为它封装了 Operation 的各个执行状态。
上面的代码:
// Operation 执行状态的 KVO
var state = State.ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
didSet {
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
}
}
状态默认是 isReady
如果设置 Operation 的状态为 isExecuting
。那么此时的状态是: isReady = false
, isExecuting = true
。执行的顺序是这样的:
- Will change for isReady.
- Will change for isExecuting
- Did change for isReady.
- Did change for isExecuting.
之所以这样设置是因为 Operation 需要直到 isReady 和 isExecuting 的值的改变情况。
-
一个具体的网络图片下载 Operation
import UIKit
typealias ImageOperationCompletion = ((Data?, URLResponse?, Error?) -> Void)?
final class NetworkImageOperation: AsyncOperation {
var image: UIImage?
private let url: URL
private let completion: ImageOperationCompletion
init(url: URL, completion: ImageOperationCompletion = nil) {
self.url = url
self.completion = completion
super.init()
}
convenience init?(string: String, completion: ImageOperationCompletion = nil) {
guard let url = URL(string: string) else { return nil }
self.init(url: url, completion: completion)
}
override func main() {
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
defer { self.state = .finished }
if let completion = self.completion {
completion(data, response, error)
return
}
guard error == nil, let data = data else { return }
self.image = UIImage(data: data)
}.resume()
}
}
上面的代码很简单,它直接继承了之前封装的异步 Operation 类:AsyncOperation。下面是 TableView 获取图片的一个情况:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "normal", for: indexPath) as! PhotoCell
cell.display(image: nil)
let op = NetworkImageOperation(url: urls[indexPath.row])
op.completionBlock = {
DispatchQueue.main.async {
guard let cell = tableView.cellForRow(at: indexPath) as? PhotoCell
else { return }
cell.isLoading = false
cell.display(image: op.image)
}
}
queue.addOperation(op)
return cell
}
图片异步下载,下载好了以后回到主线程更新Cell。
在之前我们介绍了给图片添加过滤功能的一个 Operation, 现在又介绍了一个图片异步下载的 Operation。下面会介绍怎么把这两个 Operation 合在一起使用,那样就能比较明确的看到多线程Operation的好处了,毕竟目前介绍的实现其实使用 GCD 也是没什么区别的😂。