为了实现对并行任务的进一步控制, 除了GCD之外,iOS还提供了另外一个多线程机制,叫做:Operation Queue。它是基于GCD的面向对象封装,来了解一下它的用法。
NSOperation和NSOperationQueue
和GCD一样,我们可以把operation queue也认为是一个队列,这个队列在iOS中用NSOperationQueue表示,添加到operation queue中的任务除了会被并行执行外,还有它们自己的特点:
- 它们并不遵从FIFO的原则;
- 它们不再是简单的closure,而是被封装成了一个NSOperation类;
但是NSOperation是一个抽象类,我们不能直接生成一个NSOperation对象,iOS有两个基于NSOperation实现了两个具象类:
- NSBlockOperation - 我们可以把它理解为是一个closure的封装;
- NSInvocationOperation - 在Objective-C中通过selector指定要调用的任务;
但是,由于使用NSInvocationOperation可能带来的类型安全以及ARC安全的问题,Apple从iOS 8.1开始去掉了这个类。大家可以在iOS 8.1 API Diffs看到这个变更。因此,我们只使用NSBlockOperation就好了。
通过Operation queue下载图片
我们这次通过operation queue下载图片。首先,我们创建一个operation queue对象:
class ViewController: UIViewController {
// Omit for simplicity...
@IBAction func downloadImages(sender: UIButton) {
var queue = NSOperationQueue()
}
// Omit for simplicity...
}
然后,我们有两种不同的方式把任务添加到队列中:第一种是使用addOperationWithBlock直接向operation queue中添加一个closure,它会被自动转换成NSBlockOperation:
class ViewController: UIViewController {
// Omit for simplicity...
@IBAction func downloadImages(sender: UIButton) {
var queue = NSOperationQueue()
queue.addOperationWithBlock({
let img1 =
Downloader.downloadImageWithURL(self.imageUrls[0])
NSOperationQueue.mainQueue().addOperationWithBlock({
self.image1.image = img1
self.image1.clipsToBounds = true
})
})
}
// Omit for simplicity...
}
在上面这段代码中,有一点是需要注意的:更新UI的代码仍旧要放到app的主线程中完成。我们使用NSOperationQueue.mainQueue()得到app的主线程队列,然后使用addOperationWithBlock把更新UI的任务插入到主线程队列。
或者,我们可以先手工创建一个NSBlockOperation对象,然后把它添加到operation queue。这样做的好处是,我们可以设置NSBlockOperation对象的completionBlock属性,在任务完成的时候得到通知,例如打印一个字符串:
class ViewController: UIViewController {
// Omit for simplicity...
@IBAction func downloadImages(sender: UIButton) {
var queue = NSOperationQueue()
// 1. Create a NSBlockOperation object
let op2 = NSBlockOperation(block: {
let img2 = Downloader.downloadImageWithURL(self.imageUrls[1])
NSOperationQueue.mainQueue().addOperationWithBlock({
self.image2.image = img2
self.image2.clipsToBounds = true
})
})
// 2. Set finish callback
op2.completionBlock = { print("image2 downloaded") }
// 3. Add to operation queue manually
queue.addOperation(op2)
}
// Omit for simplicity...
}
我们可以用类似的方法添加下载图片3和4的代码。然后Command + R编译执行,就可以看到图片在operation queue中并行下载了,打开控制台,我们还可以看到图片2/3/4下载完成的打印结果。
设置任务之间的关联性
在一开始我们就提到过,operation queue提供了更细粒度的任务控制,我们可以设置一个队列中不同任务的关联性,让它们先后执行。例如,我们要让图片按照432的顺序下载,同时让图片1保持并行下载,可以使用NSBlockOperation的addDependency方法:
class ViewController: UIViewController {
// Omit for simplicity...
@IBAction func downloadImages(sender: UIButton) {
var queue = NSOperationQueue()
// Omit for simplicity...
let op4 = NSBlockOperation(block: {
let img4 = Downloader.downloadImageWithURL(self.imageUrls[3])
NSOperationQueue.mainQueue().addOperationWithBlock({
self.image4.image = img4
self.image4.clipsToBounds = true
})
})
op4.completionBlock = { print("image4 downloaded") }
op3.addDependency(op4)
op2.addDependency(op3)
queue.addOperation(op4)
queue.addOperation(op3)
queue.addOperation(op2)
}
}
这里我们把添加NSBlockOperation到operation queue的操作,放到了addDependency后面。确保它们在执行之前有正确的依赖关系。然后Command + R编译执行,就可以看到我们期望的结果了。
取消任务的执行
除了设置依赖关系,我们可以随时取消一个operation queue中所有的任务,但是取消的结果根据任务的状态会有所不同:
- 所有已经完成的任务,取消操作不会有任何结果;
- 如果一个任务被取消,所有和它关联的任务也会被取消;
- 任务被取消之后,completionBlock仍旧会被执行;
为了演示这个功能,我们先添加一个Cancel按钮:
然后给它添加下面的事件处理方法:
class ViewController: UIViewController {
// Omit for simplicity...
let queue = NSOperationQueue()
@IBAction func cancelDownload(sender: AnyObject) {
self.queue.cancelAllOperations()
}
}
在上面的代码中,为了能在cancelDownload里访问我们创建的operation queue,我们要把之前创建的queue变成一个class property。
然后Command + R编译执行,我们在第4个图片下载完之后点击“Cancel”,就会发现:
由于图片1、3和4下载完成了,所以取消它没有任何效果;
图片2的下载被取消了;
如果我们按的快一些,可以赶在图片3的下载完成之前取消,那么图片2的下载也会被取消。为了获取任务的取消状态,我们可以访问NSBlockOperation对象的cancelled属性,如果任务取消成功,它的值为true,否则是false:
class ViewController: UIViewController {
// Omit for simplicity...
let queue = NSOperationQueue()
class ViewController: UIViewController {
// Omit for simplicity...
@IBAction func downloadImages(sender: UIButton) {
let op3 = NSBlockOperation(block: {
let img3 = Downloader.downloadImageWithURL(self.imageUrls[2])
NSOperationQueue.mainQueue().addOperationWithBlock({
self.image3.image = img3
self.image3.clipsToBounds = true
})
})
op3.completionBlock = { print("image3 is cancelled: \(op3.cancelled)") }
}
}
然后Command + R重新编译执行,这次我们点击"Download"后快速点击“Cancel”,如果我们可以拦截到image3的下载,就会发现image2的下载同样会被取消,此时op3.cancelled的值就是true。
网友评论