Soheil Azarpour
Learn how to use NSOperations in your app!

学习如何在你的app中使用NSOperations!

这篇博客是由iOS个人开发者Soheil Moayedi Azarpour发布的。

每个人都会在使用iOS或者Mac app,点击按钮或者输入文本时,有过让人沮丧的经历,突然间,用户交互界面停止了响应。

你真幸运 – 你只能盯着沙漏或者旋转的风火轮一段时间直到能够再次和UI界面交互为止!挺讨厌的,不是吗?

在一款移动端iOS程序中,用户期望你的app可以即时地响应他们的触摸操作,然而当它不响应时,app就会让人觉得反应迟钝,通常会导致不好的评价。

然而说的容易做就难。一旦你的app需要执行多个任务,事情很快就会变得复杂起来。在主运行回路中并没有很多时间去执行繁重的工作,并且还有一直提供可响应的UI界面。

可怜的开发者要怎么做呢?一种方法是通过并发操作将部分任务从主线程中撤离。并发操作意味着你的程序可以在操作中同时执行多个流(或者线程)- 这样,当你执行任务时,交互界面可以保持响应。

一种在iOS中执行并发操作的方法,是使用NSOperation和NSOperationQueue类。在本教程中,你将学习如何使用它们!你会先创建一款不使用多线程的app,这样它会变得响应非常迟钝。然后改进程序,添加上并行操作 – 并且希望 – 可以提供一个交互响应更好的界面给用户!

在开始阅读这篇教程之前,先阅读我们的 Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial会很有帮助。然而,因为本篇教程比较通俗易懂,所以也可以不必阅读这篇文章。

背景知识

在你学习这篇教程之前,有几个技术概念需要先解决下。

也许你听说过并发和并行操作。从技术角度来看,并发是程序的属性,而并行运作是机器的属性。并行和并发是两种分开的概念。作为程序员,你不能保证你的代码会在能并行执行你的代码的机器上运行。然而,你可以设计你的代码,让它使用并发操作。

首先,有必要定义几个术语:

  • 任务:一项需要完成的,简单,单一的任务。
  • 线程:一种由操作系统提供的机制,允许多条指令在一个单独的程序中同时执行。
  • 进程:一段可执行的代码,它可以由几个线程组成。

注意:在iPhone和Mac中,线程功能是由POSIX Threads API(或者pthreads)提供的,它是操作系统的一部分。这是相当底层的东西,你会发现很容易犯错;也许线程最坏的地方就是那些极难被发现的错误吧!

Foundation 框架包含了一个叫做NSThread的类,他更容易处理,但是使用NSThread管理多个线程仍然是件令人头疼的事情。NSOperation和NSOperationQueue是更高级别的类,他们大大简化了处理多个线程的过程。

在这张图中,你可以看到进程,线程和任务之间的关系:

Process, Thread and Task

进程,线程和任务

正如你看到的,一个进程包含多个可执行的线程,而且每个线程可以同时执行多项任务。

在这张图中,线程2执行了读文件的操作,而线程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教程。

在Mac OS X v10.6和iOS4之前,NSOperation 与 NSOperationQueue 不同于GCD,他们使用了完全不同的机制。从Mac OS X v10.6和iOS4开始,NSOperation 和 NSOperationQueue是建立在GCD上的。作为一种通例,苹果推荐使用最高级别的抽象,然而当评估显示有需要时,会突然降到更低级别。

以下是对两者的快速比较,它会帮助你决定何时何地去使用GCD或者NSOperation和NSOperationQueue;

  • GCD是一种轻量级的方法来代表将要被并发执行的任务单位。你并不需要去计划这些任务单位;系统会为你做计划。在块(block)中添加依赖会是一件令人头疼的事情。取消或者暂停一个块会给一个开发者产生额外的工作!:]
  • NSOperation和NSOperationQueue 对比GCD会带来一点额外的系统开销,但是你可以在多个操作(operation)中添加附属。你可以重用操作,取消或者暂停他们。NSOperation和 Key-Value Observation (KVO)是兼容的;例如,你可以通过监听NSNotificationCenter去让一个操作开始执行。

初步的工程模型

在工程的初步模型中,你有一个由字典作为其数据来源的table view。字典的关键字是图片的名字,每个关键字的值是图片所在的URL地址。本工程的目标是读取字典的内容,下载图片,应用图片滤镜操作,最后在table view中显示图片。

以下是该模型的示意图:

Preliminary Model

初步模型

实现 – 你可能会首先想到的方法…

注意:
如果你不想先创建一个非线程版本的工程,而是想直接进入多线程方向,你可以跳过这一节,下载我们在本节中创建的第一版本工程。

所有的图片来自stock.xchng。在数据源中的某些图片是有意命名错误,这样就有例子去测试下载图片失败的情况。

启动Xcode并使用iOSApplicationEmpty Application模版创建一个新工程,然后点击下一步。将它命名为ClassicPhotos。选择Universal, 勾选上Use Automatic Reference Counting(其他都不要选),然后点击下一步。将工程保存到任意位置。

从Project Navigator中选择ClassicPhoto工程。选择Targets ClassicPhotosBuild Phases 然后展开Link Binary with Libraries。使用+按钮添加Core Image framework(你将需要Core Image来做图像滤镜处理)。

在Project Navigator中切换到AppDelegate.h 文件,然后导入ListViewController文件 — 它将会作为root view controller,接下来你会定义它。ListViewController是UITableViewController的子类。

#import "ListViewController.h"

切换到AppDelegate.m文件,找到application:didFinishLaunchingWithOptions:方法。Init和alloc一个ListViewController的实例变量。将它包在UINavigationController中,然后设置它为UIWindow的root view controller.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];self.window.backgroundColor = [UIColor whiteColor];/*ListViewController is a subclass of UITableViewController.We will display images in ListViewController.Here, we wrap our ListViewController in a UINavigationController, and set it as the root view controller.*/ListViewController *listViewController = [[ListViewController alloc] initWithStyle:UITableViewStylePlain];UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:listViewController];self.window.rootViewController = navController;[self.window makeKeyAndVisible];return YES;
}

注意: 如果你之前还没有这样创建过一个用户界面,那么这就是在不需要使用Storyboards或者Interface Builder的情况下,用纯代码形式去创建一个用户界面的方法。

接下来创建一个UITableViewController的子类,然后命名它为ListViewController. 切换到ListViewController.h文件,并对它做以下修改:

//1
#import UIKit/UIKit.h
#import CoreImage/CoreImage.h// 2
#define kDatasourceURLString @"http://www.raywenderlich.com/downloads/ClassicPhotosDictionary.plist"// 3
@interface ListViewController : UITableViewController// 4
@property (nonatomic, strong)NSDictionary *photos; // main data source of controller
@end

让我们一段一段地过一遍上面的代码:

  1. 导入UIKit和Core Image。
  2. 方便起见,定义kDatasourceURLString为数据源文件所在位置的URL字符串。
  3. 通过替换NSObject为UITableViewController,让ListViewController继承UITableViewController。
  4. 定义NSDictionary的一个实例。这会是数据源。

现在,切换到ListViewController.m文件,添加以下代码:

@implementation ListViewController
//1
@synthesize photos = _photos;#pragma mark -
#pragma mark - Lazy instantiation// 2
- (NSDictionary *)photos {if (!_photos) {NSURL *dataSourceURL = [NSURL URLWithString:kDatasourceURLString];_photos = [[NSDictionary alloc] initWithContentsOfURL:dataSourceURL];}return _photos;
}#pragma mark -
#pragma mark - Life cycle- (void)viewDidLoad {// 3self.title = @"Classic Photos";// 4self.tableView.rowHeight = 80.0;[super viewDidLoad];
}- (void)viewDidUnload {// 5[self setPhotos:nil];[super viewDidUnload];
}#pragma mark -
#pragma mark - UITableView data source and delegate methods// 6
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {    NSInteger count = self.photos.count;return count;
}// 7
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {return 80.0;
}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {static NSString *kCellIdentifier = @"Cell Identifier";UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];if (!cell) {cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier];cell.selectionStyle = UITableViewCellSelectionStyleNone;}// 8NSString *rowKey = [[self.photos allKeys] objectAtIndex:indexPath.row];NSURL *imageURL = [NSURL URLWithString:[self.photos objectForKey:rowKey]];NSData *imageData = [NSData dataWithContentsOfURL:imageURL];UIImage *image = nil;// 9if (imageData) {UIImage *unfiltered_image = [UIImage imageWithData:imageData];image = [self applySepiaFilterToImage:unfiltered_image];}cell.textLabel.text = rowKey;cell.imageView.image = image;return cell;
}#pragma mark -
#pragma mark - Image filtration// 10
- (UIImage *)applySepiaFilterToImage:(UIImage *)image {CIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)];UIImage *sepiaImage = nil;CIContext *context = [CIContext contextWithOptions:nil];CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil];CIImage *outputImage = [filter outputImage];CGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]];sepiaImage = [UIImage imageWithCGImage:outputImageRef];CGImageRelease(outputImageRef);return sepiaImage;
}@end

好的!这里做了很多事情。别害怕 – 以下是对代码原理的解释:

  1. 对 photos 变量做了Synthesize操作.
  2. 使用了惰性实例化去加载数据源,比如photos 字典。
  3. 为view设置了title属性。
  4. 将table view中的row高度设定为80.0个像素点。
  5. 当ListViewController被卸载时,将photos变量设置为nil。
  6. 返回被显示的row数量。
  7. 这是UITableViewDelegate的可选方法。为了达到更好的视觉效果,将每个row的高度设定为80.0。默认值为44.0。
  8. 从字典中获取key值,根据key的value值创建NSURL,然后以NSData类型下载图片数据。
  9. 如果你成功下载完数据,创建image对象,并且使用褐色滤光镜。
  10. 这个方法对image对象使用了褐色滤光镜。如果你想了解更多关于Core Image filters的内容,你可以阅读Beginning Core Image in iOS 5 Tutorial。

就是这样!试试吧!编译运行工程。很美,褐色的照片 – 但是…他们..看起来…很…慢! 虽然很好看,但是你只能在等待图片加载的时候去吃点零食打发时间了. :]

ClassicPhotos-stalled-screenshot

ClassicPhotos (缓慢的版本)

是时候想想如何提升用户体验了!

线程

每一个应用程序至少有一个主线程。线程的工作就是去执行一系列的指令。在Cocoa Touch中,主线程包含应用程序的主运行回路。几乎所有你写的代码都会在主线程中执行,除非你特别创建了一个单独的线程,并在这个新线程中执行代码。

线程有两个显著的特征:

  1. 每个线程都有访问你的应用程序资源的同等权限;它包括访问除了局部变量之外的所有的对象。所以,任何对象都可能被任意线程修改,使用并且改变。
  2. 没有办法可以去预测一个线程会运行多久 — 或者哪个线程会首先完成!

所以,知道这些技术很重要,它们可以去攻克难点,防止意外的错误!:] 以下是对多线程应用时面临的挑战介绍 – 以及一些如何有效解决它们的提示。

  • 资源竞争:当每个线程都去访问同一段内存时,会导致所谓的资源竞争问题。当有多个并发线程访问共享数据时,首先访问内存数据的线程会改变共享数据 – 而且并不能保证哪个线程会首先访问到内存数据。你也许会假设有一个局部变量拥有你的线程最后一次写到共享内存的值,但是另一个线程也许会同时改变了共享内存的数据,然后你的局部变量就过时了!如果你知道这种情况会存在你的代码中(例如你会从多个线程同时读/写数据),就应该使用互斥锁。互斥代表互相排斥。你可以通过使用 “@synchronized block”将实例变量包围起来,创建一个互斥锁。这样你就可以确保在互斥锁中的代码一次只能被一个线程访问:

     @synchronized (self) { myClass.object = value; }

    在以上代码中“Self”被称为一个“信号量”。当一个线程要范围这段代码时,它会检查其他的线程是否也在访问“self”。如果没有线程在访问“self”,这块代码会被执行;否则这段线程会被限制访问直到这个互斥锁解除为止。

  • 原子性:你也许在property声明中见过很多次“nonatomic”。当你将一个property声明为atomic时,通常会把它包裹在一个@synchronized块中,确保它是线程安全的。当然,这种方法会添加一些额外的系统开销。为了更清楚的解释它,以下是一个关于atomic property的初步实现:
    // If you declare a property as atomic ...
    @property (atomic, retain) NSString *myString;// ... a rough implementation that the system generates automatically,
    // looks like this:
    - (NSString *)myString {@synchronized (self) {return [[myString retain] autorelease];
}}

    在上面的代码中,“retain”和“autorelease”被当做返回值来使用,它们被多个线程访问了,而且你不希望这个对象在多个调用之间被释放了。

    所以,你先把它的值retain一下,然后把它放在自动释放池中。你可以在苹果的技术文档里面了解到更多关于 线程安全的内容。只要是大部分iOS程序员不想费心去发掘它的话,都值得去了解下。重要提示:这是一个很好的面试问题!:]

    大部分的UIKit properties都不是线程安全的。想看下一个类是否是线程安全的,可以看看API文档。如果API文档没有提到任何关于线程安全的内容,你可以假设这个类是非线程安全的。

    按常规,如果你正在执行一个二级的线程,而且你要对UIKit对象做操作,可以使用performSelectorOnMainThread

  • 死锁:一个线程被停滞,无限期地等待永远不会发生的条件。例如,如果两个线程在互相执行synchronized代码,每一个线程就会等待另一个线程完成并且打开锁。但是这种情况永远不会发生,这样两个线程都会成为死锁。
  • 困乏时间:这会发生在有太多的线程同时执行,系统会停滞不前。NSOperationQueue有一个属性,让你设置并发线程的数量。

NSOperation API

NSOperation 类有一个相当简短的声明。要定制一个操作,可以遵循以下步骤:

  1. 继承NSOperation类
  2. 重写“main”方法
  3. 在“main”方法中创建一个“autoreleasepool”
  4. 将你的代码放在“autoreleasepool”中

创建你自己的自动释放池的原因是,你不能访问主线程的自动释放池,所以你应该自己创建一个。以下是一个例子:

#import Foundation/Foundation.h@interface MyLengthyOperation: NSOperation
@end
@implementation MyLengthyOperation- (void)main {// a lengthy operation@autoreleasepool {for (int i = 0 ; i < 10000 ; i++) {NSLog(@"%f", sqrt(i));}}
}@end

上面的例子代码展示了ARC语法在自动释放池中的使用。你现在必须使用ARC了!:]

在线程操作中,你从来都不能明确知道,一个操作什么时候会开始,要持续多久才能结束。在大多数时候,如果用户滑动离开了页面,你并不想在后台执行一个操作 – 没有任何的理由让你去执行。这里关键是要经常地检查NSOperation类的isCancelled属性。例如,在上面的例子程序中,你会这样做:

@interface MyLengthyOperation: NSOperation
@end@implementation MyLengthyOperation
- (void)main {// a lengthy operation@autoreleasepool {for (int i = 0 ; i < 10000 ; i++) {// is this operation cancelled?if (self.isCancelled)break;NSLog(@"%f", sqrt(i));}}
}
@end

要取消一个操作,你可以调用NSOperation的cancel方法,展示如下:

// In your controller class, you create the NSOperation
// Create the operation
MyLengthyOperation *my_lengthy_operation = [[MyLengthyOperation alloc] init];
.
.
.
// Cancel it
[my_lengthy_operation cancel];

NSOperation类还有其他的方法和属性:

  • 开始(start):通常,你不会重写这个方法。重写“start”方法需要相对复杂的实现,你还需要注意像isExecuting,isFinished,isConcurrent和isReady这些属性。当你将一个操作添加到一个队列当中时(一个NSOperationQueue的实例,接下来会讨论的),这个队列会在操作中调用“start”方法,然后它会做一些准备和“main”方法的后续操作。假如你在一个NSOperation实例中调用了“start”方法,如果没有把它添加到一个队列中,这个操作会在main loop中执行。
  • 从属性(Dependency):你可以让一个操作从属于其他的操作。任何操作都可以从属于任意数量的操作。当你让操作A从属于操作B时,即使你调用了操作A的“start”方法,它会等待操作B结束后才开始执行。例如:
MyDownloadOperation *downloadOp = [[MyDownloadOperation alloc] init]; // MyDownloadOperation is a subclass of NSOperation
MyFilterOperation *filterOp = [[MyFilterOperation alloc] init]; // MyFilterOperation  is a subclass of NSOperation[filterOp addDependency:downloadOp];

要删除依赖性:

  [filterOp removeDependency:downloadOp];
  • 优先级(Priority):有时候你希望在后台运行的操作并不是很重要的,它可以以较低的优先级执行。可以通过使用“setQueuePriority:”方法设置一个操作的优先级。

    [filterOp setQueuePriority:NSOperationQueuePriorityVeryLow];

    其他关于设置线程优先级的选择有: NSOperationQueuePriorityLow, NSOperationQueuePriorityNormal, NSOperationQueuePriorityHigh和NSOperationQueuePriorityVeryHigh.
    当你添加了操作到一个队列时,在对操作调用“start”方法之前,NSOperationQueue会浏览所有的操作。那些有较高优先级的操作会被先执行。有同等优先级的操作会按照添加到队列中的顺序去执行(先进先出)。
    (历史注释:在1997年,火星车中的嵌入式系统遭遇过优先级反转问题,也许这是说明正确处理优先级和互斥锁的最昂贵示例了。想对这一事件的背景知识有更多的了解,可以看这个网址: http://research.microsoft.com/en-us/um/people/mbj/Mars_Pathfinder/Mars_Pathfinder.html )

  • Completion block:在NSOperation 类中另一个有用的方法叫setCompletionBlock:。一旦操作完成了,如果你还有一些事情想做,你可以把它放在一个块中,并且传递给这个方法。这个块会在主线程中执行。
     [filterOp removeDependency:downloadOp];

其他一些关于处理线程的提示:

  • 如果你需要传递一些值和指针到一个线程中,创建你自己的指定初始化方法是一个很好的尝试:

    #import Foundation/Foundation.h@interface MyOperation : NSOperation-(id)initWithNumber:(NSNumber *)start string:(NSString *)string;@end
  • 如果你的操作需要有一个返回值或者对象,声明一个委托方法是不错的选择。记住委托方法必须在主线程中返回。然而,因为你要继承NSOperation类,你必须先将这个操作类强制转换为NSObject对象。可以按照以下步骤去做:
    [(NSObject *)self.delegate performSelectorOnMainThread:(@selector(delegateMethod:)) withObject:object waitUntilDone:NO];
  • 要经常检查isCancelled属性。如果操作不需要被执行了,你就不想在后台去运行它了!
  • 你并不需要重写“start”方法。然而,如果你决定去重写“start”方法,就必须处理好像isExecuting, isFinished, isConcurrent 和 isReady这些属性。否则你的操作类不会正确的运作。
  • 你一旦添加了一个操作到一个队列(NSOperationQueue的一个实例)中,就要负责释放它(如果你不使用ARC的话)。NSOperationQueue 获得操作对象的所有权,调用“start”方法,然后结束时负责释放它。
  • 你不能重用一个操作对象。一旦它被添加到一个队列中,你就丧失了对它的所有权。如果你想再使用同一个操作类,就必须创建一个新的实例变量。
  • 一个结束的操作不能被重启。
  • 如果你取消了一个操作,它不会马上就发生。它会在未来的某个时候某人在“main”函数中明确地检查isCancelled == YES 时被取消掉;否则,操作会一直执行到完成为止。
  • 一个操作是否成功地完成,失败了,或者是被取消了,isFinished的值总会被设置为YES。所以千万不要觉得isFinished == YES就表示所有的事情都顺利完成了 — 特别的,如果你在代码里面有从属性(dependencies),就要更加注意!

NSOperationQueue API

NSOperationQueue 也有一个相当简单的界面。它甚至比NSOperation还要简单,因为你不需要去继承它,或者重写任何的方法 — 你可以简单创建一个。给你的队列起一个名字会是一个不错的做法;这样你可以在运行时识别出你的操作队列,并且让调试变得更简单:

NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
myQueue.name = @"Download Queue";
  • 并发操作:队列和线程是两个不同的概念。一个队列可以有多个线程。每个队列中的操作会在所属的线程中运行。举个例子你创建一个队列,然后添加三个操作到里面。队列会发起三个单独的线程,然后让所有操作在各自的线程中并发运行。
    到底有多少个线程会被创建?这是个很好的问题!:] 这取决与硬件。默认情况下,NSOperationQueue类会在场景背后施展一些魔法,决定如何在特定的平台下运行代码是最好的,并且会尽量启用最大的线程数量。考虑以下的例子。假设系统是空闲的,并且有很多的可用资源,这样NSOperationQueue会启用比如8个同步线程。下次你运行程序,系统会忙于处理其他不相关的操作,它们消耗着资源,然后NSOperationQueue只会启用两个同步线程了。
  • 并发操作的最大值:你可以设定NSOperationQueue可以并发运行的最大操作数。NSOperationQueue会选择去运行任何数量的并发操作,但是不会超过最大值。
    myQueue.MaxConcurrentOperationCount = 3;

    如果你改变了主意,想将MaxConcurrentOperationCount设置回默认值,你可以执行下列操作:

    myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
  • 添加操作:一个操作一旦被添加到一个队列中,你就应该通过传送一个release消息给操作对象(如果使用了手动引用计数,非ARC的话),然后队列会负责开始这个操作。从这点上看,什么时候调用“start”方法由这个队列说了算。
    [myQueue addOperation:downloadOp];
    [downloadOp release]; // manual reference counting
  • 待处理的操作:任何时候你可以询问一个队列哪个操作在里面,并且总共有多少个操作在里面。记住只有那些等待被执行的操作,还有那些正在运行的操作,会被保留在队列中。操作一完成,就会退出队列。
    NSArray *active_and_pending_operations = myQueue.operations;
    NSInteger count_of_operations = myQueue.operationCount;
  • 暂停队列:你可以通过设定setSuspended:YES来暂停一个队列。这样会暂停所有在队列中的操作 — 你不能单独的暂停操作。要重新开始队列,只要简单的setSuspended:NO。
    // Suspend a queue
    [myQueue setSuspended:YES];
    .
    .
    .
    // Resume a queue
    [myQueue setSuspended: NO];
  • 取消操作:要取消一个队列中的所有操作,你只要简单的调用“cancelAllOperations”方法即可。还记得之前提醒过经常检查NSOperation中的isCancelled属性吗?
    原因是“cancelAllOperations”并没有做太多的工作,他只是对队列中的每一个操作调用“cancel”方法 — 这并没有起很大作用!:] 如果一个操作并没有开始,然后你对它调用“cancel”方法,操作会被取消,并从队列中移除。然而,如果一个操作已经在执行了,这就要由单独的操作去识别撤销(通过检查isCancelled属性)然后停止它所做的工作。
      [myQueue cancelAllOperations];
  • addOperationWithBlock: 如果你有一个简单的操作不需要被继承,你可以将它当做一个块(block)传递给队列。如果你需要从块那里传递回任何数据,记得你不应该传递任何强引用的指针给块;相反,你必须使用弱引用。而且,如果你想要在块中做一些跟UI有关的事情,你必须在主线程中做。

    UIImage *myImage = nil;// Create a weak reference
    __weak UIImage *myImage_weak = myImage;// Add an operation as a block to a queue
    [myQueue addOperationWithBlock: ^ {// a block of operationNSURL *aURL = [NSURL URLWithString:@"http://www.somewhere.com/image.png"];NSError *error = nil;NSData *data = [NSData dataWithContentsOfURL:aURL options:nil error:&error];If (!error)[myImage_weak imageWithData:data];// Get hold of main queue (main thread)[[NSOperationQueue mainQueue] addOperationWithBlock: ^ {myImageView.image = myImage_weak; // updating UI}];}];

重新定义模型

是时候重新定义初步的非线程模型了!如果你仔细看下初步的模型,你会看到有三个线程区域可以改进。通过把这三个区域区分开来,然后把它们各自放在一个单独的线程中,主线程会获得解脱,并且可以保持对用户交互的迅速响应。

注意:如果你不能马上理解为什么你的app运作得这么慢 — 而且有时候这并不明显 — 你应该使用Instruments工具。然而,这需要另一篇教程去讲解它了!:]

Improved model

改进的模型

为了摆脱你的程序的瓶颈限制,你需要一个特定的线程去响应用户交互事件,一个线程专门用于下载数据源和图片,还有一个线程用于执行图片滤镜处理。在新的模型中,app在主线程中开始,并且加载一个空白的table view。同时,app会开始另一个线程去下载数据源。

一旦数据源下载完毕,你会告诉table view重新加载自己。这会在主线程中完成。这个时候,table view知道有多少行,而且知道需要显示的图片的URL地址,但是它还没有实际的图片!如果你在这个时候马上开始下载所有的图片,这会非常没有效率,因为你一下子不需要所有的图片!

怎样可以把它弄得更好?

一个更好的模型就是去下载在当前屏幕可见的row的图片。所以你的代码首先会问table view哪些row是可见的,然后才会开始下载过程。还有,图片滤镜处理会在图片下载完成后才开始。因此,代码应该等待出现有一个待滤镜处理的图片时才开始进行图片滤镜处理。

为了让app的反应变得更加灵敏,代码会在图片下载完毕后马上显示,而不会等待进行滤镜处理。一旦图片的滤镜处理完成,就会更新UI以显示滤镜处理过的图片。以下是整个处理过程的控制流示意图:

NSOperation Control Flow

控制流程

为了达到这些目标,你需要去监测图片是否正在下载,或者已经完成了下载,还是图片的滤镜处理已经完成了。你还需要去监测每个操作的状态,以及判断它是一个下载操作还是一个滤镜处理操作,这样你才能在用户滚动table view的时候去做取消,中止或者恢复操作。

好的!现在你准备好开始写代码了!:]

打开之前的工程,添加一个命名为 PhotoRecord的NSObject新子类到工程中。打开PhotoRecord.h文件,然后添加以下代码到头文件中:

#import UIKit/UIKit.h
// because we need UIImage@interface PhotoRecord : NSObject@property (nonatomic, strong) NSString *name;  // To store the name of image
@property (nonatomic, strong) UIImage *image; // To store the actual image
@property (nonatomic, strong) NSURL *URL; // To store the URL of the image
@property (nonatomic, readonly) BOOL hasImage; // Return YES if image is downloaded.
@property (nonatomic, getter = isFiltered) BOOL filtered; // Return YES if image is sepia-filtered 
@property (nonatomic, getter = isFailed) BOOL failed; // Return Yes if image failed to be downloaded@end

是不是觉得上面的语法挺熟悉的?每一个property都有一个getter和setter方法。像这样去指定getter方法仅仅是让它的命名更加明确。

切换到PhotoRecord.m文件,然后添加以下代码:

@implementation PhotoRecord@synthesize name = _name;
@synthesize image = _image;
@synthesize URL = _URL;
@synthesize hasImage = _hasImage;
@synthesize filtered = _filtered;
@synthesize failed = _failed;- (BOOL)hasImage {return _image != nil;
}- (BOOL)isFailed {return _failed;
}- (BOOL)isFiltered {return _filtered;
}@end

要监测每一个操作的状态,你需要一个单独的类。创建另一个命名为PendingOperations的NSObject新类。切换到PendingOperations.h文件,然后添加以下代码:

#import Foundation/Foundation.h@interface PendingOperations : NSObject@property (nonatomic, strong) NSMutableDictionary *downloadsInProgress;
@property (nonatomic, strong) NSOperationQueue *downloadQueue;@property (nonatomic, strong) NSMutableDictionary *filtrationsInProgress;
@property (nonatomic, strong) NSOperationQueue *filtrationQueue;@end

这个头文件也挺简单。你申明了两个字典去监测活跃和等待的下载与滤镜操作。字典的key代表table view row的indexPath,然后字典的value会是两个单独的ImageDownloader和ImageFiltration实例。

注意:你可能会对为什么要监测所有的活跃和等待操作感到好奇。难道不能通过对 [NSOperationQueue operations]的查询来访问它们吗?是的,但是在本工程中,这样做的话效率不是很高。

每次你需要去用有等待操作的行(row)的indexPath去和可见行的indexPath作对比时,你需要使用几个迭代循环,这样的话会是一个很耗资源的操作。通过申明一个额外的NSDictionary实例,你可以方便的了解等待操作(operations),而不需要执行没有效率的循环操作(operations)。

切换到PendingOperations.m文件,然后添加以下代码:

@implementation PendingOperations
@synthesize downloadsInProgress = _downloadsInProgress;
@synthesize downloadQueue = _downloadQueue;@synthesize filtrationsInProgress = _filtrationsInProgress;
@synthesize filtrationQueue = _filtrationQueue;- (NSMutableDictionary *)downloadsInProgress {if (!_downloadsInProgress) {_downloadsInProgress = [[NSMutableDictionary alloc] init];}return _downloadsInProgress;
}- (NSOperationQueue *)downloadQueue {if (!_downloadQueue) {_downloadQueue = [[NSOperationQueue alloc] init];_downloadQueue.name = @"Download Queue";_downloadQueue.maxConcurrentOperationCount = 1;}return _downloadQueue;
}- (NSMutableDictionary *)filtrationsInProgress {if (!_filtrationsInProgress) {_filtrationsInProgress = [[NSMutableDictionary alloc] init];}return _filtrationsInProgress;
}- (NSOperationQueue *)filtrationQueue {if (!_filtrationQueue) {_filtrationQueue = [[NSOperationQueue alloc] init];_filtrationQueue.name = @"Image Filtration Queue";_filtrationQueue.maxConcurrentOperationCount = 1;}return _filtrationQueue;
}@end

这里,你重写了一些getter方法去利用惰性实例化,所以你并不需要真的去给实例变量分配内存空间,直到他们被访问为止。你还要给两个队列初始化和分配内存空间 — 一个用于下载操作,一个用于滤镜处理 — 然后设定他们的属性(properties),所以当你在另外的类中访问他们时,你不需要担心他们的初始化操作。 maxConcurrentOperationCount变量在本教程中设定为1。

现在,是时候处理下载和滤镜处理操作了。创建一个命名为ImageDownloader的NSOperatoin子类。切换到ImageDownloader.h文件,然后添加以下代码:

#import Foundation/Foundation.h// 1
#import "PhotoRecord.h"// 2
@protocol ImageDownloaderDelegate;@interface ImageDownloader : NSOperation@property (nonatomic, assign) id  delegate;// 3
@property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readonly, strong) PhotoRecord *photoRecord;// 4
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id) theDelegate;@end@protocol ImageDownloaderDelegate // 5
- (void)imageDownloaderDidFinish:(ImageDownloader *)downloader;
@end

第二部分

以下是对上面代码的注解:

  1. 导入PhotoRecord.h文件,这样你就可以在下载成功后,单独地设置PhotoRecord变量的图片属性(image property)。如果下载失败,设定它的failed值为YES。
  2. 申明一个delegate,这样一旦操作完成了,你可以通知调用者(caller)。
  3. 为了方便起见,申明了indexPathInTableView变量,这样一旦操作结束了,调用者就会有一个属于操作的引用。
  4. 申明一个初始化方法。
  5. 在你的delegate方法中,将整个类作为对象传递回给调用者,这样调用者就可以访问indexPathInTableView和photoRecord变量了。因为你需要去将操作对象(operation)强制转换为NSObject类型,然后在主线程中返回,delegate方法的变量不能超过一个。

切换到ImageDownloader.m文件,然后做以下修改:

// 1
@interface ImageDownloader ()
@property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readwrite, strong) PhotoRecord *photoRecord;
@end@implementation ImageDownloader
@synthesize delegate = _delegate;
@synthesize indexPathInTableView = _indexPathInTableView;
@synthesize photoRecord = _photoRecord;#pragma mark -
#pragma mark - Life Cycle- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>)theDelegate {if (self = [super init]) {// 2self.delegate = theDelegate;self.indexPathInTableView = indexPath;self.photoRecord = record;}return self;
}#pragma mark -
#pragma mark - Downloading image// 3
- (void)main {// 4@autoreleasepool {if (self.isCancelled)return;NSData *imageData = [[NSData alloc] initWithContentsOfURL:self.photoRecord.URL];if (self.isCancelled) {imageData = nil;return;}if (imageData) {UIImage *downloadedImage = [UIImage imageWithData:imageData];self.photoRecord.image = downloadedImage;}else {self.photoRecord.failed = YES;}imageData = nil;if (self.isCancelled)return;// 5[(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageDownloaderDidFinish:) withObject:self waitUntilDone:NO];}
}@end

通过代码注释,你将看到上面的代码在做下面的操作:

  1. 申明一个私有的接口,你可以改变实例变量的属性为读写(read-write)。
  2. 设定properties属性。
  3. 经常检查isCancelled变量,确保操作即时结束。
  4. 苹果推荐使用@autoreleasepool块,而不是alloc和init NSAutoreleasePool变量,因为blocks更加有效率。你也许会使用NSAutoreleasePool,这样也行。
  5. 将operation对象强制转换为NSobject类型,然后在主线程中通知调用者(caller)。

现在,继续创建一个NSOperation的子类,用来处理图片滤镜操作吧!

创建另一个命名为 ImageFiltration的NSOperation新子类。打开 ImageFiltration.h文件,添加以下代码:


// 1
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>
#import "PhotoRecord.h"// 2
@protocol ImageFiltrationDelegate;@interface ImageFiltration : NSOperation@property (nonatomic, weak) id <ImageFiltrationDelegate> delegate;
@property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readonly, strong) PhotoRecord *photoRecord;- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate;@end@protocol ImageFiltrationDelegate <NSObject>
- (void)imageFiltrationDidFinish:(ImageFiltration *)filtration;
@end

再次的,以下是对上面代码的注释:

  1. 因为你要去对UIImage实例执行滤镜处理操作,就需要导入UIKit和CoreImage框架。还需要导入PhotoRecord头文件。与ImageDownloader类似,你想让调用者(caller)使用初始化函数去进行alloc和init操作。申明一个delegate,一旦操作完成了,通知调用者。
  2. 申明一个delegate,一旦操作完成了,通知调用者。

切换到ImageFiltration.m文件,添加以下代码:


@interface ImageFiltration ()
@property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readwrite, strong) PhotoRecord *photoRecord;
@end@implementation ImageFiltration
@synthesize indexPathInTableView = _indexPathInTableView;
@synthesize photoRecord = _photoRecord;
@synthesize delegate = _delegate;#pragma mark -
#pragma mark - Life cycle- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate {if (self = [super init]) {self.photoRecord = record;self.indexPathInTableView = indexPath;self.delegate = theDelegate;}return self;
}#pragma mark -
#pragma mark - Main operation- (void)main {@autoreleasepool {if (self.isCancelled)return;if (!self.photoRecord.hasImage)return;UIImage *rawImage = self.photoRecord.image;UIImage *processedImage = [self applySepiaFilterToImage:rawImage];if (self.isCancelled)return;if (processedImage) {self.photoRecord.image = processedImage;self.photoRecord.filtered = YES;[(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageFiltrationDidFinish:) withObject:self waitUntilDone:NO];}}}#pragma mark -
#pragma mark - Filtering image- (UIImage *)applySepiaFilterToImage:(UIImage *)image {// This is expensive + time consumingCIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)];if (self.isCancelled)return nil;UIImage *sepiaImage = nil;CIContext *context = [CIContext contextWithOptions:nil];CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil];CIImage *outputImage = [filter outputImage];if (self.isCancelled)return nil;// Create a CGImageRef from the context// This is an expensive + time consumingCGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]];if (self.isCancelled) {CGImageRelease(outputImageRef);return nil;}sepiaImage = [UIImage imageWithCGImage:outputImageRef];CGImageRelease(outputImageRef);return sepiaImage;
}@end

上面的实现方法和ImageDownloader类似。图片的滤镜处理的实现方法和你之前在ListViewController.m文件中的一样。它被移动到这里以便可以在后台作为一个单独的操作完成。你应该经常检查isCancelled参数;在任何系统资源消耗较大的函数调用前后去调用这个滤镜处理函数,是不错的做法。一旦滤镜处理结束了,PhotoRecord实例的值会被恰当的设置好,然后主线程的delegate被通知了。

很好!现在你已经有了在后台线程中执行操作(operations)的所有工具和基础了。是时候回到view controller然后恰当的修改它,以便它可以利用好这些新优势。

注意:在动手之前,你要下载AFNetworking library from GitHub.

AFNetworking库是建立在NSOperation 和 NSOperatinQueue之上的。它提供给你很多便捷的方法,以便你不需要为普通的任务,比如在后台下载一个文件,创建你自己的操作。

当需要从互联网下载一个文件的时候,在适当的位置写一些代码来检查错误是个不错的做法。下载数据源,一个只有4kBytes 的property list,不是什么大问题,你并不需要操心去为它创建一个子类。然而,你不能假设会有一个可靠持续的网络连接。

苹果为此提供了NSURLConnection类。使用它会是一项额外的工作,特别是当你只是想下载一个小的property list时。AFNetworking是一个开源代码库,提供了一种非常方便的方式去实施这类任务。你要传入两个块(blocks),一个在操作成功时传入,另一个在操作失败时传入。接下来你会看到相关的实践例子。

要添加这个库到工程中,选择File > Add Files To …,然后浏览选择你下载好的AFNetworking文件夹,最后点击“Add”。确保选中了“Copy items into destination group’s folder”选项!是的,你正在使用ARC,但是AFNetworking还没有从陈旧的手动管理内存的泥潭中爬出来。

如果你遵循着安装指南,就可以避免编译错误,如果你不遵循的话,你会在编译时去处理非常多的错误。每一个AFNetworking模块需要在你的Target’s Build Phases标签包含 “-fno-objc-arc”字段,它在 Compiler Flags部分下面。

要实现它,在导航栏(在左手边)点击“PhotoRecords”。在右手边,选择“Targets”下面的“ClassicPhotos”。从标签栏选择“Build Phases”。在它下面,选择三角形展开“Compile Sources”项。选上属于AFNetworking的所有文件。敲击Enter键,一个对话框就会弹出来。在对话框中,输入 “fno-objc-arc”,然后点击“Done”。

切换到 ListViewController.h文件,然后根据以下内容更新头文件:


// 1
#import <UIKit/UIKit.h>
// #import <CoreImage/CoreImage.h> ... you don't need CoreImage here anymore.
#import "PhotoRecord.h"
#import "PendingOperations.h"
#import "ImageDownloader.h"
#import "ImageFiltration.h"
// 2
#import "AFNetworking/AFNetworking.h"#define kDatasourceURLString @"https://sites.google.com/site/soheilsstudio/tutorials/nsoperationsampleproject/ClassicPhotosDictionary.plist"// 3
@interface ListViewController : UITableViewController <ImageDownloaderDelegate, ImageFiltrationDelegate>// 4
@property (nonatomic, strong) NSMutableArray *photos; // main data source of controller// 5
@property (nonatomic, strong) PendingOperations *pendingOperations;
@end

这里发生了什么事?以下要点对上面的代码做了解释:

  1. 你可以从ListViewController头文件中删除CoreImage,因为你不再需要它了。然而,你需要导入PhotoRecord.h文件,PendingOperations.h,ImageDownloader.h和ImageFiltration.h文件。
  2. 这里是对AFNetworking库的引用。
  3. 确保让ListViewController遵从 ImageDownloader和ImageFiltration的delegate方法。
  4. 你不再需要这样的数据源。你将要使用property list来创建PhotoRecord的实例。所以,将“photos”类从NSDictionary修改为NSMutableArray,这样你就可以更新图片数组了。
  5. 这个property被用来监测等待操作(operations)。

切换到ListViewController.m文件,然后根据以下内容进行更新:

// Add this to the beginning of ListViewController.m
@synthesize pendingOperations = _pendingOperations;
.
.
.
// Add this to viewDidUnload
[self setPendingOperations:nil];

在“photos”的惰性初始化之前,添加“pendingOperations”的惰性初始化:

- (PendingOperations *)pendingOperations {if (!_pendingOperations) {_pendingOperations = [[PendingOperations alloc] init];}return _pendingOperations;
}

现在来到“photos”的惰性初始化,并做以下修改:

- (NSMutableArray *)photos {if (!_photos) {// 1NSURL *datasourceURL = [NSURL URLWithString:kDatasourceURLString];NSURLRequest *request = [NSURLRequest requestWithURL:datasourceURL];// 2AFHTTPRequestOperation *datasource_download_operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];// 3[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];// 4[datasource_download_operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {// 5NSData *datasource_data = (NSData *)responseObject;CFPropertyListRef plist =  CFPropertyListCreateFromXMLData(kCFAllocatorDefault, (__bridge CFDataRef)datasource_data, kCFPropertyListImmutable, NULL);NSDictionary *datasource_dictionary = (__bridge NSDictionary *)plist;// 6NSMutableArray *records = [NSMutableArray array];for (NSString *key in datasource_dictionary) {PhotoRecord *record = [[PhotoRecord alloc] init];record.URL = [NSURL URLWithString:[datasource_dictionary objectForKey:key]];record.name = key;[records addObject:record];record = nil;}// 7self.photos = records;CFRelease(plist);[self.tableView reloadData];[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];} failure:^(AFHTTPRequestOperation *operation, NSError *error){// 8// Connection error messageUIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Oops!"message:error.localizedDescriptiondelegate:nilcancelButtonTitle:@"OK"otherButtonTitles:nil];[alert show];alert = nil;[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];}];// 9[self.pendingOperations.downloadQueue addOperation:datasource_download_operation];}return _photos;
}

以上代码做了一些操作。下面的内容是对代码完成内容的一步步解析:

  1. 创建NSURL和NSURLRequest对象,指向数据源的位置。
  2. 使用AFHTTPRequestOperation类,用request对象来alloc和init它。
  3. 在下载数据时,通过启动网络活动指示器(network activity indicator)来提供用户反馈。
  4. 通过使用setCompletionBlockWithSuccess:failure:,你可以添加两个块(blocks):一个给操作成功的情况,另一个给操作失败的情况。
  5. 在成功的块中,以NSData的数据格式下载property list, 然后通过使用toll-free briding桥,将参数强制转换成CFDataRef和CFPropertyList, 再将property list文件转换成NSDictionary。
  6. 创建一个NSMutableArray,然后在字典中循环申明所有的objects和key,创建一个PhotoRecord实例,然后保存它到数组中。
  7. 一旦完成了,将_photo对象指向records数组,重新加载table view然后停止网络活动指示器。你还要释放”plist”实例变量。
  8. 也许你的操作会不成功,这时要显示一条消息给用户看。
  9. 最后,添加 “datasource_download_operation”到PendingOperations的“downloadQueue”中。

来到 tableView:cellForRowAtIndexPath:方法,根据以下内容做修改:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {static NSString *kCellIdentifier = @"Cell Identifier";UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];if (!cell) {cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier];cell.selectionStyle = UITableViewCellSelectionStyleNone;// 1UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];cell.accessoryView = activityIndicatorView;}// 2PhotoRecord *aRecord = [self.photos objectAtIndex:indexPath.row];// 3if (aRecord.hasImage) {[((UIActivityIndicatorView *)cell.accessoryView) stopAnimating];cell.imageView.image = aRecord.image;cell.textLabel.text = aRecord.name;}// 4else if (aRecord.isFailed) {[((UIActivityIndicatorView *)cell.accessoryView) stopAnimating];cell.imageView.image = [UIImage imageNamed:@"Failed.png"];cell.textLabel.text = @"Failed to load";}// 5else {[((UIActivityIndicatorView *)cell.accessoryView) startAnimating];cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];cell.textLabel.text = @"";[self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];}return cell;
}

同样的,花点时间看下下面的评论解析:

  1. 要提供反馈给用户,创建一个UIActivityIndicatorView然后把它设置为cell的accessory view。
  2. 数据源包含PhotoRecord的所有实例。根据行(row)的indexPath参数,从photos数组中获取并创建相应的PhotoRecord实例。
  3. 检查PhotoRecord。看它的图片是否已经下载完了,显示了图片,图片的名字,然后停止了活动指示器 (activity indicator)。
  4. 如果下载图片失败,显示一个预留图片来提示失败情况,然后停止活动指示器(activity indicator)。
  5. 否则,图片还没有被下载下来。开始下载和图片滤镜处理操作(它们现在还没有被实现),然后显示一个预留图片表示你正在对它进行处理。启动活动指示器(activity indicator)来提醒用户有操作正在进行。

现在是时候来实现负责启动操作的方法了。如果你还没有实现它,可以在ListViewController.m文件中删除旧的“applySepiaFilterToImage:”实现方法。

来到代码的结尾,实现下列方法:


// 1
- (void)startOperationsForPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {// 2if (!record.hasImage) {// 3[self startImageDownloadingForRecord:record atIndexPath:indexPath];}if (!record.isFiltered) {[self startImageFiltrationForRecord:record atIndexPath:indexPath];}
}

以上的代码相当直接,但是有些东西要解释下:

  1. 为了保持简洁,你要根据它的indexPath值,传入一个需要操作(operations)的PhotoRecord实例。
  2. 检查一下看看它是否有一张图片;如果是,就不管它。
  3. 如果它没有一张图片,通过调用 startImageDownloadingForRecord:atIndexPath:(它会被简短的实现出来)方法,开始下载图片。你也可以对滤镜操作做同样的处理:如果图片还没有被滤镜处理过,可以调用startImageFiltrationForRecord:atIndexPath:(他也会被简短的实现出来)方法。
注意: 下载图片和滤镜处理图片的方法是单独实现的,因为有可能当图片正在下载时,用户会将图片滚动掉,然后你还没有对图片做滤镜处理。这样下次用户回到同一行时,你就不需要重新下载图片;只需要去实现图片的滤镜处理了!很有效的一招!:]

现在你需要去实现以上代码段的startImageDownloadingForRecord:atIndexPath:方法。记住你创建了一个自定义的类,PendingOperations,用于检测操作(operations)。在这里你开始使用它了。

- (void)startImageDownloadingForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {// 1if (![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPath]) {// 2 // Start downloadingImageDownloader *imageDownloader = [[ImageDownloader alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self];[self.pendingOperations.downloadsInProgress setObject:imageDownloader forKey:indexPath];[self.pendingOperations.downloadQueue addOperation:imageDownloader];}
}- (void)startImageFiltrationForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {// 3if (![self.pendingOperations.filtrationsInProgress.allKeys containsObject:indexPath]) {// 4// Start filtrationImageFiltration *imageFiltration = [[ImageFiltration alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self];// 5ImageDownloader *dependency = [self.pendingOperations.downloadsInProgress objectForKey:indexPath];if (dependency)[imageFiltration addDependency:dependency];[self.pendingOperations.filtrationsInProgress setObject:imageFiltration forKey:indexPath];[self.pendingOperations.filtrationQueue addOperation:imageFiltration];}
}

好的!以下是简短的解析,以确保你理解了以上代码的工作原理。

  1. 首先,检查特定的indexPath看是否已经有一个操作在downloadsInProgress中了。如果有,就不管它。
  2. 如果没有,使用指定的初始化函数创建一个ImageDownloader的实例,然后设置ListViewController作为它的delegate。传入恰当的indexPath和一个指针给PhotoRecord的实例,然后把它添加到下载队列中。你还要把它添加到downloadsInProgress中,来帮助监测事情。
  3. 同样的,检查看是否有任何的滤镜处理操作在特定的indexPath项中进行。
  4. 如果没有,使用指定的初始化函数开始一个。
  5. 这里的代码有点巧妙。你首先必须检查看这个特定的indexPath项是否有一个等待的下载任务;如果是,你可以基于该特定项创建这个滤镜操作。

很好!你现在需要去实现ImageDownloader和ImageFiltration的delegate方法了。将下列代码添加到ListViewController.m文件的末尾:

- (void)imageDownloaderDidFinish:(ImageDownloader *)downloader {// 1NSIndexPath *indexPath = downloader.indexPathInTableView;// 2PhotoRecord *theRecord = downloader.photoRecord;// 3[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];// 4[self.pendingOperations.downloadsInProgress removeObjectForKey:indexPath];
}- (void)imageFiltrationDidFinish:(ImageFiltration *)filtration {NSIndexPath *indexPath = filtration.indexPathInTableView;PhotoRecord *theRecord = filtration.photoRecord;[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];[self.pendingOperations.filtrationsInProgress removeObjectForKey:indexPath];
}

所有的delegate方法都有非常相似的实现,所以这里只需要拿其中一个做讲解:

  1. 检查操作(operation)的indexPath值,看看它是一个下载操作,还是一个滤镜处理操作。
  2. 创建PhotoRecord的实例对象。
  3. 更新UI。
  4. 从downloadsInProgress(或者filtrationsInProgress)中移除操作。

更新:关于处理PhotoRecord的实例,来自论坛的“xlledo”提了一个不错的意见。因为你正在传一个指针给PhotoRecord,再给NSOperation的子类(ImageDownloader和ImageFiltration),你可以直接修改它们。所以replaceObjectAtIndex:withObject:方法在这里是多余的。

赞!

Wow! 你做到了!你的工程完成了。编译运行看看实际的提升效果!当你滚动table view的时候,app不再卡死,当cell可见时,就开始下载和滤镜处理图片了。

难道这不是很cool吗?你可以看到一点小小的努力就可以让你的应用程序的响应变得更加灵敏 — 并且让用户觉得更加有趣!

进一步地调整

你已经在本篇教程中进展很久了!你的小工程比起原来的版本变得更加反应灵敏,有了很大的提升。然而,仍然有一些细节需要去处理。你想成为一个优秀的程序员,而不仅仅是好的程序员!

你也许已经注意到当你在table view中滚动时,那些屏幕以外的cell仍然处于下载和滤镜处理的进程中。难道你没有在代码里面设置取消操作?是的,你有 — 你应该好好的利用它们!:]

回到Xcode,切换到ListViewController.m文件中。来到tableView:cellForRowAtIndexPath:的方法实现,如下所示,将[self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];放在if判断分支中:


// in implementation of tableView:cellForRowAtIndexPath:
if (!tableView.dragging && !tableView.decelerating) {[self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];
}

你告诉table view只有在它没有滚动时才开始操作(operations)。判断项是UIScrollView的properties属性,然后因为UITableView是UIScrollView的子类,它就自动地继承了这些properties属性。

现在,来到ListViewController.m文件的结尾,实现下面的UIScrollView委托方法:

#pragma mark -
#pragma mark - UIScrollView delegate- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {// 1[self suspendAllOperations];
}- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {// 2if (!decelerate) {[self loadImagesForOnscreenCells];[self resumeAllOperations];}
}- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {// 3[self loadImagesForOnscreenCells];[self resumeAllOperations];
}

以下是对上面代码的解析:

  1. 一旦用户开始了滚动操作,你会想中止所有的操作(operations),然后看下用户想看什么。接下来会实现suspendAllOperations方法。
  2. 如果decelerate的值为NO,表示用户停止了拖动table view的操作。所以你想恢复中止了的操作(operations),取消屏幕以外的cell的操作,开始屏幕内cell的操作。接下来我们会实现loadImagesForOnscreenCells和resumeAllOperations方法。
  3. 这个delegate方法告诉你table view停止了滚动,所以你会做跟第2步一样的操作。

好的!现在,在ListViewController.m文件的结尾添加上suspendAllOperations,resumeAllOperations和loadImagesForOnscreenCells方法的实现:


#pragma mark -
#pragma mark - Cancelling, suspending, resuming queues / operations- (void)suspendAllOperations {[self.pendingOperations.downloadQueue setSuspended:YES];[self.pendingOperations.filtrationQueue setSuspended:YES];
}- (void)resumeAllOperations {[self.pendingOperations.downloadQueue setSuspended:NO];[self.pendingOperations.filtrationQueue setSuspended:NO];
}- (void)cancelAllOperations {[self.pendingOperations.downloadQueue cancelAllOperations];[self.pendingOperations.filtrationQueue cancelAllOperations];
}- (void)loadImagesForOnscreenCells {// 1NSSet *visibleRows = [NSSet setWithArray:[self.tableView indexPathsForVisibleRows]];// 2NSMutableSet *pendingOperations = [NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgress allKeys]];[pendingOperations addObjectsFromArray:[self.pendingOperations.filtrationsInProgress allKeys]];NSMutableSet *toBeCancelled = [pendingOperations mutableCopy];NSMutableSet *toBeStarted = [visibleRows mutableCopy];// 3[toBeStarted minusSet:pendingOperations];// 4[toBeCancelled minusSet:visibleRows];// 5for (NSIndexPath *anIndexPath in toBeCancelled) {ImageDownloader *pendingDownload = [self.pendingOperations.downloadsInProgress objectForKey:anIndexPath];[pendingDownload cancel];[self.pendingOperations.downloadsInProgress removeObjectForKey:anIndexPath];ImageFiltration *pendingFiltration = [self.pendingOperations.filtrationsInProgress objectForKey:anIndexPath];[pendingFiltration cancel];[self.pendingOperations.filtrationsInProgress removeObjectForKey:anIndexPath];}toBeCancelled = nil;// 6for (NSIndexPath *anIndexPath in toBeStarted) {PhotoRecord *recordToProcess = [self.photos objectAtIndex:anIndexPath.row];[self startOperationsForPhotoRecord:recordToProcess atIndexPath:anIndexPath];}toBeStarted = nil;}

suspendAllOperations, resumeAllOperations 和 cancelAllOperations 方法都有直接的实现方式。你基本上会使用工厂方法去中止,恢复或者取消操作和队列。为了方便起见,你将它们放在单独的方法中。

LoadImagesForOnscreenCells方法有点复杂。以下是对它的解释:

  1. 获取一NSSet可见行(rows)。
  2. 获取一NSMutableSet所有的等待操作(下载和滤镜处理)。
  3. Rows(或者indexPaths)对应的开始操作(operation),等于visible rows — pendings的数量。
  4. Rows(或者indexPaths)对应的需要取消的操作,等于pendings — visible rows的数量。
  5. 循环查看需要被取消的操作,取消它们,然后从PendingOperations中移除它们的引用。
  6. 循环查看需要开始的操作,为它们中的每一个调用startOperationsForPhotoRecord:atIndexPath:方法。

最后,这个难题的最后项由ListViewController.m文件中的didReceiveMemoryWarning方法解决。


// If app receive memory warning, cancel all operations
- (void)didReceiveMemoryWarning {[self cancelAllOperations];[super didReceiveMemoryWarning];
}

编译运行工程,你会看到一个响应更加灵敏,有更好的资源管理的应用程序!给自己一点掌声吧!

ClassicPhotos-improved

ClassicPhotos (改进版本)

现在还可以做什么?

这里是工程改进后的完整代码。

如果你完成了这个工程,并且花时间真正理解了它,恭喜!相比刚阅读本教程时,你可以把自己看待成一个更有价值的iOS开发者了!大部分的开发工作室都会幸运的拥有一两个能真正理解这些原理的人。

但是注意 — 像deeply-nested blocks(块),无理由地使用线程会让维护你的代码的人难以理解。线程会引来不易察觉的bugs,只有当网络缓慢时才会出现,或者当代码运行在一个更快(或者更慢)的设备中,或者有不同内核数目的设备中。仔细认真的测试,经常使用Instruments(或者是你自己的观察)来核实引入的线程是否真的取得了性能提升。

如果你对本教程或者NSOperations有任何的意见或者问题,请加入下面的论坛讨论!

如何使用NSOperations和NSOperationQueues相关推荐

  1. ReactiveCocoa入门-part2

    ReactiveCocoa是一个框架,它能让你在iOS应用中使用函数响应式编程(FRP)技术.在本系列教程的第一部分中,你学到了如何将标准的动作与事件处理逻辑替换为发送事件流的信号.你还学到了如何转换 ...

  2. NSOperation vs大中央派遣

    本文翻译自:NSOperation vs Grand Central Dispatch I'm learning about concurrent programming for iOS. 我正在学习 ...

  3. iOS应用性能调优的25个建议和技巧【转】

    转载自:http://blog.jobbole.com/37984/ 首页 最新文章 资讯 程序员 设计 IT技术 创业 在国外 营销 趣文 特别分享 更多 > - Navigation - 首 ...

  4. [转]25个增强iOS应用程序性能的提示和技巧

    在开发iOS应用程序时,让程序具有良好的性能是非常关键的.这也是用户所期望的,如果你的程序运行迟钝或缓慢,会招致用户的差评.然而由于iOS设备的局限性,有时候要想获得良好的性能,是很困难的.在开发过程 ...

  5. iOS应用性能调优建议

    本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelofabri.com/,你还可以 ...

  6. 提高ios app性能 初中高级实践

    这篇文章来自iOS Tutorial Team 成员 Marcelo Fabri, 他是 Movile 的一个iOS开发者. Check out his personal website or fol ...

  7. iOS应用性能调优的25个建议和技巧

    写在前面 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelofabri.com/ ...

  8. 25个技巧和诀窍可以用来提高你的app性能

    [ 原文出处:http://www.raywenderlich.com/311 - ormance-tips-tricks当我们开发iOS应用时,好的性能对我们的App来说是很重要的.你的用户也希望如 ...

  9. 浅谈iOS中关于app的优化

    目录 我要给出的建议将分为三个不同的等级: 入门级. 中级和进阶级: 入门级(这是些你一定会经常用在你app开发中的建议) 1. 用ARC管理内存 2. 在正确的地方使用reuseIdentifier ...

  10. 【转】25个开发性能优化

    写在前面 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelofabri.com/ ...

最新文章

  1. 解决android引用library project错误
  2. Oracle 存储过程异常处理
  3. 安装bigsur卡在12分钟_Big Sur为什么安装不了?macOS Big Sur无法完成安装的解决办法!...
  4. maven中jar下载失败
  5. 总体方差的充分统计量_R方是否衡量预测能力或统计充分性?
  6. 计算机软件记不住设置,想知道电脑密码记不住了怎么办
  7. php7如何安装swoole,初学Swoole:PHP7安装Swoole的步骤
  8. OpenStack Hacker养成指南
  9. vue组件独享守卫钩子函数参数详解(beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave)...
  10. 安装python3.8出现ModuleNotFoundError: No module named ‘_ctypes’解决办法
  11. 简单学JAVA-Java前世今生
  12. 二十一世纪最该了解的一类人:找个极客做朋友吧
  13. 《Cocos Creator游戏实战》棋类游戏中的棋子摆放逻辑
  14. Django models中的null和blank的区别
  15. 常见Andriod游戏破解搜索关键字
  16. 人口统计、红利、康波
  17. 反欺诈类优秀文章汇总
  18. 网站关键词如何设置覆盖上百个关键词(佐伊科技分享干货)
  19. 蓝桥杯 算法训练 旅行家的预算
  20. 最新前端跨平台框架推荐,跨平台开发框架选择指南

热门文章

  1. mysql 如何去掉毫秒_mysql 如何去掉毫秒值
  2. php 豆瓣抓取,PHP抓取豆瓣读书爬虫代码
  3. 人工智能学习(十一):机器人学
  4. 核磁共振谱仪定义、发展及基本原理
  5. shell一站式攻略——history历史命令
  6. css实现手风琴图片特效
  7. [51Nod 1035 最长的循环节] 循环小数的性质
  8. 计算机网络代表第几次革命,互联网:人类生产力的第三次革命
  9. 数据结构刘畅c语言版课后答案,数据结构习题集答案--清华大学版
  10. android 清除某个通知,android清除通知栏消息