原文地址
原作者:Richard Turton 于 2014.10.7
更新于 2015.4.17 : 使用的 Xcode 6.3 和 Swift 1.2
译者说明:下面的代码示例,是我调整过的,因为用 Xcode7.3 会报错,旧代码需要适配。调整后的完整代码。
更新说明: 这篇教程被 Richard Turton 更新到了 iOS8,Xcode 6.1 和 Swift。原文是教程团队成员 Soheil Azarpour创作的。
每个人都有过在使用 iOS 或者 Mac app 时,点击按钮或输入文本突然界面停止响应的糟糕体验。
在 Mac 上,你的用户会看到一个沙漏或一个旋转的彩色轮子直到他们能再与 UI 交互。在 iOS app 上,用户期望 app 可以立马响应他们的触摸。不能响应的 app 会让人感觉不好和慢,且通常会收到坏的评价。
保持你的 app 响应灵敏说的比做的要容易的多。一旦你的 app 需要执行大量任务,事情很快就会变的复杂。在主运行循环中没有太多的时间去执行繁重的工作而且它还要提供 UI 的响应。
解决方法是通过 并发 把工作移出主线程。并发的意思就是你的程序在同一时间执行多个流(或线程)的操作,这样你的工作在执行中,界面还可以持续响应。
有一种方法是使用 iOS 里的 NSOperation 和 NSOperationQueue 类并发执行操作。在这个教程里,你将学会怎样去使用它们!你将从一个 app 完全不使用并发开始,因此它将显示的非常慢且卡顿。然后你将重新修改这个程序加上并发操作,并希望可以为用户提供一个更灵敏的界面。
准备开始
总体来说这个教程的简单项目是一个显示过滤后图片的表格。这些图片将被从网上下载下来,进行滤镜处理,然后显示到表格(table view)上。
这里是这个 app 的模型图表:
第一次尝试
下载第一个版本的项目,你将在这个教程上开始。
译者提示:下载下来的项目有一些报错,但是根据 Xcode 的提示很容易就修复了。
注意:所有的图片来自stock.xchng。数据源里有一些图片是故意找不到名字的,以便有一些下载失败的案例去练习失败的情况。
编译并运行这个项目,之后你将看到一个显示一列照片的 app。试着滑动这个列表。很痛苦是不是?
所有的操作都在 ListViewController.swift 文件里,且大部分是在 tableView(_:cellForRowAtIndexPath:) 方法里面。
看下这个方法并注意到有两件事集中到那里:
1,从网络加载图片的数据。即使是很简单的工作,app 也要等待下载完成才能继续。
2,使用 Core Image 过滤图片。这个方法使用一个深褐色来过滤图片。如果你想知道更多关于 Core Image 过滤的东西,可以查看 Beginning Core Image in Swift。
另外,你也要在第一次请求时从网上获取照片的列表:
lazy var photos = NSDictionary(contentsOfURL:dataSourceURL)
所有的工作都在程序的主线程。因为主线程也要负责用户的交互,从网络加载东西会让它一直忙碌且过滤图片正在消磨 app 的响应速度。你可以使用 Xcode 的仪器视图可以快速看到一个概览。显示 Debug navigator (Command-6) 并且在程序运行的时候选择 CPU 看到一个仪器视图。
你可以看到在 Thread 1 所有的长钉,Thread 1 就是 app 的主线程。更多的细节信息,你可以使用 instruments 来运行 app,但这是另一篇教程。
现在来思考下怎么能提升你的用户体验!
任务 Task ,线程 Thread 和进程 Process
在你扑进这个教程之前,有一点技术概念需要去理解下。我定义了几项:
- Task:一个简单的,单一的工作需要去完成。
- Thread:一种操作系统提供的机制,允许在单独的程序里在同一时间按照多组指令去执行操作。
- Process:一个可执行的大块代码,可由多个线程组成。
注意:在 iOS 和 OS X,这个线程功能被 POSIX 线程 API(或 pthreads) 提供,且是操作系统的一部分。这是一个相当底层的技术,并且你将发现它很容易出错;也许这个线程错误还极难被发现!
这个 Foundation 框架包含 NSTread 类,它更容易被处理,但是管理 NSThread 的多线程还是让人头疼。 NSOperation 和 NSOperationQueue 是高层级的类,在处理多线程的过程中非常简单。
在这张图表里,你能看到进程,线程和任务的关系:
这样你可以看到,一个进程可以包含多个执行的线程,且每个线程可以同一时间处理多个任务。
在这张图表里,thread 2 执行读取一个文件的任务,同时 thread 1 执行界面相关的代码。这是十分相似的,你怎样在 iOS 里组织你的代码-主线程应该执行界面相关的任何工作,而次要线程应该执行慢或耗时的操作,像读取文件,请求网络等。
NSOperation vs. Grand Central Dispatch (GCD)
你可能听过过Grand Central Dispatch (GCD)。概括下,GCD 是由语言的特性,运行时库,和系统的增强为在 iOS 和 OS X 的多核硬件上支持并发提供了成体系的和综合的改善。如果你想多学习一些关于 GCD 的知识,可以去读我们的 Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial
NSOperation 和 NSOperationQueue 被构建在 GCD 的上层。作为很普遍的规则, Apple 推荐使用更高级的抽象,在经过评测显示真的需要低级别时,然后降下来。
这里是对于两个技术的比较,帮助你决定什么时候在哪去使用 GCD 或 NSOperation:
- GCD 是一个轻量的方法去描述工作单元被并发执行。你不能去安排这些工作单元;系统会为你去安排这些。block 之间添加依赖会很头痛。取消和暂停一个 block 需要开发者做额外的工作!:]
- NSOperation 和 GCD 比较增加一点额外的性能消耗,但你可以在各种操作之间增加依赖且重用,取消或暂停它们。
这个教程将使用 NSOperation 因为你需要在用户停止滑动,图片离开了屏幕后,为特定的图片取消一个操作,处理表格的性能和功率消耗。即使这些操作在后台线程,如果有太多的操作在队列里等待,也是会损耗性能的。
重新定义 App 的模型
现在来重新定义下最开始的这个没有线程的模型!如果你仔细看下初始的模型,你可以看到有三个线程的区域可以被改善。分离这三个区域并且把它们放到分离线程中,主线程将被减轻工作,让它持续响应界面交互。
去解除你的程序的障碍,你将需要一个特定的线程去响应界面,一个线程专门下载数据源和图片,且用一个线程去过滤图片。在新的模型里,app 在主线程上开始并且加载一个空的 table view。在相同的时间,这个 app 启动第二个线程去下载数据源。
一旦数据源被下载下来,你将告诉 table view 去刷新自己。凡是涉及界面的这些都要在主线程完成。在这一点上,这个 table view 知道它有多少行,它知道图片的 URL 需要去显示出来,但它还没有真实的图片!如果你立马去下载所有的图片,效率会非常的低,因此你不需要一次把所有的图片都下载下来!
做什么才会更好呢?
一个更好的模型是去开始下载那些在屏幕上显示出来的行的图片。所以你的代码要先询问 table view 那些行是显示的,然后去开始下载的处理。还有,图片的滤镜处理不能在图片下载完成之前进行。因此,代码应该直到有一个没有过滤的图片等待处理时,在去开始处理图片的过滤。
为了让你的 app 显得更灵敏,一旦图片下载成功,代码要正确的显示它们。然后开始过滤图片,更新 UI 显示这个过滤后的图片。下面的图表就是这个处理的大体控制流程:
为了达到这些目的,你需要去追踪这个图片当前是否被下载,是否下载完成,或是否在过滤。你也需要去追踪每个操作的状态,是正在下载或正在过滤的操作,以至于你可以取消,暂停或恢复当每个用户滑动时。
好的!现在你要准备开始编码了!:]
打开项目,增加一个名字叫 PhotoOperations.swift 的 Swift File 到你的项目。增加下面的代码:
import UIKit
//This enum contains all the possible states a photo record can be in
enum PhotoRecordState {
case New, Downloaded,Filtered,Failed
}
class PhotoRecord {
let name:String
let url:NSURL
var state = PhotoRecordState.New
var image = UIImage(named: "Placeholder")
init(name:String, url:NSURL) {
self.name = name
self.url = url
}
}
注意: 确信 import UIKit 在文件的头部。默认,Xcode将只导入 Foundation 到 Swift 文件中。
这个简单的类将代表在 app 中显示的每个照片,连同它的当前状态,新创建的记录默认是 .New 。图片默认是一个占位符。
你需要一个单独的类去追踪每个操作的状态。增加下面的内容到 PhotoOperations.swift 的底部:
class PendingOperations {
lazy var downloadsInProgress = [NSIndexPath:NSOperation]()
lazy var downloadQueue:NSOperationQueue = {
var queue = NSOperationQueue()
queue.name = "Download queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
lazy var filtrationsInProgress = [NSIndexPath:NSOperation]()
lazy var filtrationQueue:NSOperationQueue = {
var queue = NSOperationQueue()
queue.name = "Image Filtration queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
}
这个类包含两个字典去记录表格里每一行的活动,等待下载和过滤的操作,并且有每种类型操作的两个队列。
所有的值都是懒加载(lazily),意思就是直到它们第一次获取才被初始化。这能提高你的 app 的性能。
创建 NSOperationQueue 非常的简单,正如你看到的。命名你的队列有助于你在 debugger 或者 instruments 里看到这个名字。这个maxConcurrentOperationCount 为了这个教程设置的是 1,可以让你看到操作是一个接一个完成的。你可以离开这部分允许队列去决定一次处理多少操作-这能更进一步提高性能。
队列怎样决定一次运行多少操作?这是一个好问题!:]它依赖硬件。默认,NSOperationQueue 将会在幕后做一些计算,决定代码运行在哪些特定的平台是最好的,将会启动最大可能数量的线程。
考虑下面这个例子。假定系统是闲置的,并且有大量资源可用,因此队列可以像八个同时存在的线程一样启动。下一次你运行这个程序,系统可能会忙于与其他不相关的操作,这些操作都是消耗资源,而队列只启动两个同时存在的线程。因为你设置了最大并发操作数量,在这个 app 里在同一时间只能有一个操作发生。
注意: 你可能好奇为什么不得不追踪所有的活动和挂起的操作。这个队列有一个 operations 方法返回一个包含操作的数组,所以为什么不使用这个呢?在这个项目它不能满足需求。你需要去追踪和 table view 行数关联的操作,这就需要每次都去遍历数组。把它们都存到字典里,用下标(index path)作为键(key)可以快速查找,效率还高。
现在来关心下下载和过滤操作。添加下面的代码到 PhotoOperations.swift 的底部:
class ImageDownloader: NSOperation {
//1
let photoRecord: PhotoRecord
//2
init(photoRecord:PhotoRecord) {
self.photoRecord = photoRecord
}
//3
override func main() {
//4
if self.cancelled {
return
}
//5
let imageData = NSData(contentsOfURL: self.photoRecord.url)
//6
if self.cancelled {
return
}
//7
if imageData?.length > 0 {
self.photoRecord.image = UIImage(data: imageData!)
self.photoRecord.state = .Downloaded
}
else {
self.photoRecord.state = .Failed
self.photoRecord.image = UIImage(named: "Failed")
}
}
}
NSOperation 是一个抽象类,为子类化设计。每个子类代表一个特定的任务,如图中所描述的。
这里说明下在上面的代码里每一个数字注释发生了什么:
- 增加一个 PhotoRecord 对象的常量引用关联到这个操作上。
- 创建一个指定初始化构造器,传入一个照片记录。
- main 是 NSOperation 的子类要重写的方法,实际执行工作的地方。
- 在开始时检查是否取消。操作应该在试图做长时间或密集工作之前定期的检查操作有没有被取消。
- 下载图片数据。
- 再次检查有没有被取消。
- 如果有数据,创建一个图片对象并添加到记录里,修改记录状态。如果没有数据,标记记录为失败,设置一个适当的图片。
接下来,你将创建另一个图片过滤的操作!增加下面的代码到 PhotoOperations.swift:
class ImageFiltration: NSOperation {
let photoRecord: PhotoRecord
init(photoRecord:PhotoRecord) {
self.photoRecord = photoRecord
}
override func main() {
if self.cancelled {
return
}
if self.photoRecord.state != .Downloaded {
return
}
if let filteredImage = self.applySepiaFilter(self.photoRecord.image!) {
self.photoRecord.image = filteredImage
self.photoRecord.state = .Filtered
}
}
}
这个看起来和下载操作很像,除了给图片应用一个滤镜(使用了一个未实现的方法,因此编译器会报错)替换下载代码。
增加缺失的滤镜方法到 ImageFiltration 类里:
func applySepiaFilter(image:UIImage) -> UIImage? {
let inputImage = CIImage(data:UIImagePNGRepresentation(image)!)
if self.cancelled {
return nil
}
let context = CIContext(options:nil)
let filter = CIFilter(name:"CISepiaTone")
filter!.setValue(inputImage, forKey: kCIInputImageKey)
filter!.setValue(0.8, forKey: "inputIntensity")
let outputImage = filter!.outputImage
if self.cancelled {
return nil
}
let outImage = context.createCGImage(outputImage!, fromRect: outputImage!.extent)
let returnImage = UIImage(CGImage: outImage)
return returnImage
}
这个图片的滤镜使用的是之前在 ListViewController 里的相同的实现。它被移动到了这里因此它可以作为单独的操作在后台。再一次,你应该频繁的检查操作是否被取消;一个好的实践是在任何需要昂贵方法调用的前后去做检查。一旦过滤完成,你可以设置值给照片记录实例。
好的!现在你有了所有的工具和基础来处理需要后台任务的操作。现在返回到视图控制器并且修改它利用这些新的好处。
切换到 ListViewController.swift 并且删除 lazy var photos属性,增加下面的引用替换它:
var photos = [PhotoRecord]()
let pendingOperations = PendingOperations()
这是最早创建的 PhotoDetails 对象的数组,并且 PendingOperations 对象去管理这些操作。
增加一个新的方法到这个类去下载照片的属性列表(plist):
func fetchPhotoDetails() {
let request = NSURLRequest(URL: dataSourceURL!)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { (response, data, error) in
if data != nil {
let datasourceDictionary = try? NSPropertyListSerialization.propertyListWithData(data!, options: NSPropertyListMutabilityOptions.Immutable, format: nil) as! NSDictionary
for(key,value) in datasourceDictionary! {
let name = key as? String
let url = NSURL(string:value as? String ?? "")
if name != nil && url != nil {
let photoRecord = PhotoRecord(name:name!, url:url!)
self.photos.append(photoRecord)
}
}
self.tableView.reloadData()
}
if error != nil {
let alert = UIAlertView(title:"Oops!",message:error!.localizedDescription, delegate:nil, cancelButtonTitle:"OK")
alert.show()
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
这个方法创建了一个异步的 web 请求,在完成的时候,在主队列回调 block。把在下载完属性列表的数据解析成一个 NSDictionary 并且再处理成 PhotoRecord 对象的数组。在这你还没有立马使用 NSOperation,但你用 NSOperationQueue.mainQueue() 获取到了主队列。
在 viewDidLoad 的底部调用这个新的方法:
fetchPhotoDetails()
接下来,找到 tableView(_:cellForRowAtIndexPath:) 并且用下面的实现替代它:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath)
//1
if cell.accessoryView == nil {
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
cell.accessoryView = indicator
}
let indicator = cell.accessoryView as! UIActivityIndicatorView
//2
let photoDetails = photos[indexPath.row]
//3
cell.textLabel?.text = photoDetails.name
cell.imageView?.image = photoDetails.image
//4
switch (photoDetails.state){
case .Filtered:
indicator.stopAnimating()
case .Failed:
indicator.stopAnimating()
cell.textLabel?.text = "Failed to load"
case .New, .Downloaded:
indicator.startAnimating()
if (!tableView.dragging && !tableView.decelerating) {
startOperationsForPhotoRecord(photoDetails,indexPath:indexPath)
}
}
return cell
}
花一些时间去阅读下下面注释的解释:
- 为用户提供反馈,创建一个 UIActivityIndicatorView 并且设置它做为 cell 的 accessory view。
- 数据源包含 PhotoRecord 实例。基于当前行的 indexPath 获取正确的一个实例。
- 这个 cell 的文本标签(label)一直是相同的,这个设置的图片是在 PhotoRecord 被处理过的,因此你可以在这一起设置它们,不论这个纪录的状态是什么。
- 检查这个纪录。设置适当的活动指示器和文本,并且开始执行操作(还没有实现)。
你可以删除 applySepiaFilter 的实现,因为不会被使用了。增加下面的方法到这个类开始这些操作:
func startOperationsForPhotoRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath) {
switch (photoDetails.state) {
case .New:
startDownloadForRecord(photoDetails, indexPath: indexPath)
case .Downloaded:
startFiltrationForRecord(photoDetails, indexPath: indexPath)
default:
NSLog("do nothing")
}
}
这里你将传入一个带它的 index path 的 PhotoRecord 实例。依赖照片记录的状态,你开始选择下载或过滤其中的一步。
注意:下载和过滤图片的方法是分开实现的,因为有一种情况是用户可以滚动正在下载的图片并且你还没有去过滤图片。下一次用户回到相同行时,你不需要重新下载图片;你只需要过滤图片就可以了!
现在你需要去实现上面调用的这两个方法。记住,你创建了一个自定义的类,PendingOperations,去追踪操作;现在你可以实际去使用它了!增加下面的方法到这个类:
func startDownloadForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath) {
//1
if pendingOperations.downloadsInProgress[indexPath] != nil {
return
}
//2
let downloader = ImageDownloader(photoRecord: photoDetails)
//3
downloader.completionBlock = {
if downloader.cancelled {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
})
}
//4
pendingOperations.downloadsInProgress[indexPath] = downloader
//5
pendingOperations.downloadQueue.addOperation(downloader)
}
func startFiltrationForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
if pendingOperations.filtrationsInProgress[indexPath] != nil{
return
}
let filterer = ImageFiltration(photoRecord: photoDetails)
filterer.completionBlock = {
if filterer.cancelled {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
})
}
pendingOperations.filtrationsInProgress[indexPath] = filterer
pendingOperations.filtrationQueue.addOperation(filterer)
}
好的!这有个列表来确认你理解了上面代码做了什么:
- 首先,检查特定的 indexPath 在 downloadsInProgress 里有没有,如果有忽略它。
- 如果没有,使用自定义构造器创建一个 ImageDownloader 实例。
- 在操作完成时增加一个将被执行的完成回调 block。这是一个很棒的地方让你的 app 知道操作已经完成了。重点注意,这个完成回调 block 在操作取消之后也会执行,因此在做任何事之前你必须检查这个属性是否取消了,你也不能保证完成回调 block 在哪个线程上被调用,所以你需要使用 GCD 回到主线程去出发一个 table view 的刷新。
- 增加一个操作到 downloadsInProgress 去帮助追踪任务。
- 添加操作到下载队列。这是实际怎样得到这些操作开始运行-这个队列一但你添加操作到里面,就会为你调度。
过滤图片的方法用的是相同的模式,除了使用 ImageFiltration 和 filtrationsInProgress 来追踪操作。作为一个练习,你可以尝试摆脱这个重复代码:]
你做的!你的项目完成了。编译并运行看到了你做的改进!当你通过 table view 滚动时,你的程序不会在卡顿,开始下载图片且过滤它们让它们可见。
是不是很酷?你看到了为你的程序更灵敏,为用户得到更多的乐趣这条很长的路上做的一点努力!
更好的调整
在这个教程中你走了很长的路!你的小项目反应灵敏且比原来的版本提高了很多。然而还有一些小细节要关心。你想成为一个伟大的程序员,而不是一个好的!
你将发现在 table view 滚动中,这些离开屏幕的单元格还在处理下载和过滤。如果你快速的滚动,单元格会很快的从列表中返回,即使它们不可见,程序会忙于下载和过滤图片。理想情况下,程序应该取消掉过滤不在屏幕上的单元格并且优先哪些正在显示的。
在你的代码里放置了取消吗?是的,你做了-现在你应该去使用它们!:]
回到 Xcode ,并且打开 ListViewController.swift。去实现 tableView(_:cellForRowAtIndexPath:),在一个 if 条件里调用 startOperationsForPhotoRecord 。
像下面这样:
if (!tableView.dragging && !tableView.decelerating) {
startOperationsForPhotoRecord(photoDetails,indexPath:indexPath)
}
你告诉 table view 只有在 table view 不滚动的时候才开始操作。UIScrollView 有实际的属性,而且因为 UITableView 是 UIScrollView 的子类,你自动继承了这些属性。
接下来,在这个类里增加实现 UIScrollView 的代理方法:
override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
//1
suspendAllOperations()
}
override func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// 2
if !decelerate {
loadImagesForOnscreenCells()
resumeAllOperations()
}
}
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
// 3
loadImagesForOnscreenCells()
resumeAllOperations()
}
快速的看下关于上面代码的解释:
- 用户一开始滑动,你就将暂停所有的操作并且看看用户想看到什么。你将等一会儿实现 suspendAllOperations 。
- 如果 decelerate 这个值是 false ,意思就是用户停止了拖动这个 table view。为此你想恢复暂停的操作,取消掉没有显示的 cell 的操作,并且开始当前显示的 cell 的操作。你将实现 loadImagesForOnscreenCells 和 resumeAllOperations 。
- 这个代理方法告诉你 table view 停止了滑动,因此你将做和第二步一样的事。
现在,在 ListViewController.swift 里增加缺失的这些方法的实现:
func suspendAllOperations () {
pendingOperations.downloadQueue.suspended = true
pendingOperations.filtrationQueue.suspended = true
}
func resumeAllOperations () {
pendingOperations.downloadQueue.suspended = false
pendingOperations.filtrationQueue.suspended = false
}
func loadImagesForOnscreenCells () {
//1
if let pathsArray = tableView.indexPathsForVisibleRows {
//2
var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys)
allPendingOperations.unionInPlace(pendingOperations.filtrationsInProgress.keys)
//3
var toBeCancelled = allPendingOperations
let visiblePaths = Set(pathsArray )
toBeCancelled.subtractInPlace(visiblePaths)
//4
var toBeStarted = visiblePaths
toBeStarted.subtractInPlace(allPendingOperations)
// 5
for indexPath in toBeCancelled {
if let pendingDownload = pendingOperations.downloadsInProgress[indexPath] {
pendingDownload.cancel()
}
pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
if let pendingFiltration = pendingOperations.filtrationsInProgress[indexPath] {
pendingFiltration.cancel()
}
pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
}
// 6
for indexPath in toBeStarted {
let indexPath = indexPath as NSIndexPath
let recordToProcess = self.photos[indexPath.row]
startOperationsForPhotoRecord(recordToProcess, indexPath: indexPath)
}
}
}
suspendAllOperations 和 resumeAllOperations 有一个简单的实现。NSOperationQueues 可以被暂停,设置 suspended 属性为 true。这将暂停这个队列的所有的操作-你不能暂停各自的操作。
loadImagesForOnscreenCells 有一点复杂。这是发生什么:
- 从一个包含了在 table view 里所有正在显示的行的 index paths 的数组开始。
- 所有的下载任务加上所有的过滤任务构成一个包含所有待定操作的集合。
- 构造一组所有要被取消操作的下标索引 index paths。从所有的操作开始,然后删除那些显示行的下标索引 index paths。这就只留下包含所有不在屏幕上显示的行的一组操作。
- 构造一组需要它们的操作开始的下标索引 index paths。以所有正在显示的行的下标索引开始,然后删除掉已经在待定操作里的一些操作。
- 循环这些要被取消的操作,取消它们,并且从 PendingOperations 中删除它们的引用。
- 循环这些要开始的操作,逐一调用 startOperationsForPhotoRecord 。
构建并运行,你应该有了一个更反应灵敏且能更好管理资源的程序!给自己一轮掌声!
注意你在完成滚动的时候,在显示的行的图片将立刻开始处理。
从这里去哪儿
这是完整版本的项目
译者说明: 原文的完整版会有报错,我修复好了,上传到了 Github 上,而且里面有对添加依赖的实现。
修复后的完整版本项目
如果你完成了这个项目并且花了时间去真正的理解它,恭喜!你可以考虑自己是一个更有价值的 iOS 开发者和刚开始教程时比!
但是要当心-像深层次的嵌套 block,不必要的在项目中使用多线程会让不得不维护你代码的人感到难懂。线程可以引入微妙的 bug,可能永远都不会出现,直到你的网络很慢或代码运行在一个快(或慢)的设备上或一个多核的设备上。非常小心测试(或你自己观察)去验证引入的线程真的是一个改进。
有一个有用的操作不能被忽略就是依赖。你能使一个操作依赖其它一个或多个操作。这个操作直到所有的依赖操作都完成了才开始。例子:
// MyDownloadOperation is a subclass of NSOperation
let downloadOperation = MyDownloadOperation()
// MyFilterOperation is a subclass of NSOperation
let filterOperation = MyFilterOperation()
filterOperation.addDependency(downloadOperation)
删除依赖:
filterOperation.removeDependency(downloadOperation)
这个项目的代码可以使用依赖来简化或提高吗?用你的新技能去尝试一下:]有个重要的事要注意,如果这个操作它的依赖被取消了,那么这个操作还是会开始,除非它们自然的完成了。你将需要记住这点。
如果你有一些评论和问题关于这个教程或是 NSOperations,请加入到下面的论坛的讨论里。