iOS多线程之 Operation

798 阅读9分钟
  • 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。执行的顺序是这样的:

  1. Will change for isReady.
  2. Will change for isExecuting
  3. Did change for isReady.
  4. 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 也是没什么区别的😂。