原文地址:
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
本文由 大侠自来也(泰然教程组) 翻译,转载请注明出处!!!
每个人应该都有使用某款ios或者mac的app的时候出现未响应的现象吧。如果是mac下面的app,要是比较幸运的话,那还会出现无敌风火轮,直到你能够操作才消失。 如果是ios的app,就只能等了,有些时候还可能就这样卡闪退了,这样就会给用户很差的用户体验。
解释这个现象倒是很简单:就是你的app需要一些消耗大量cpu计算时间的任务的时候,在主线程里面就基本上没时间来处理你的UI交互了,所以看起来就卡了。
一般的一个解决办法就是通过并发处理来让当前复杂的计算离开当前的主线程,也就是说使用多线程来执行你的任务。这样的话,用户交互就会有反应,不会出现卡的情况。
还有一种在ios中并发处理的方法就是使用NSOperation和NSOperationQueue。在这篇教程里面,你将会学习如何使用他们。为了看到他的效果,首先我们会创建一个一点也不使用多线程的app,所以你将会看到这个app运行时是如此的不流畅,交互性如此的不好。然后我们会重写这个app,这个时候会加上并发处理,会给你提供良好的人机交互感受。
在开始这篇教程的时候,要是你去读一下ios官方的多线程和GCD教程,会对你有很大的帮助的。不过这篇教程是比较简单的,所以你也可以不用去读刚刚的那个教程,不过建议去看看,很好的。
背景
在开始这篇教程的时候,有一些技术概念需要普及一下。你应该听说过并发处理和并行处理。从技术点上来看,并发是程序的性质,并行是硬件的性质。所以并行和并发其实是两个不同的概念。作为一个程序员,你永远不能保证你的代码将会运行在一台能够使用并行处理的的机器上。但是你可以设计你的代码以至于你可以使用并发处理。(这里简单用一个比喻来说明一下并发和并行。并发就是:假如有三个人,每个人一个水果,但是在他们面前只有一把水果刀,于是每个人都会轮流来使用这把刀来削水果。并行就是:有三把水果刀了,每个人都可以干自己的事,而不用去等待别人。所以并行效率会很高,但是受硬件限制,并发其实就是多线程)。
首先,知道一些专业术语是很重要的:
作业: 一些需要被处理的简单的工作。
线程: 在一个应用程序里,由操作系统提供的,包含了很多可执行指令的集合。
进程: 一块可执行的二进制代码,由许多线程组成。
注意: 在iphone和mac上面,线程功能是由POSIX线程API(或者pthreads)提供的,并且也是操作系统的一部分。这个是相当底层的接口,所以使用的话,是非常容易犯错的,而且这个错误是很难被找到的。
有一个基本的framework包含了一个叫NSThread的类,这个类非常容易使用,但是在管理多线程上面NSThread还是比较头疼。NSOperation 和NSOperationQueue是一个高度封装的类,简化了操作多线程的难度。
在下面这个图标里面,你能够看到进程,线程和作业的关系:
正如你看的一样,一个进程包含了许多可以执行的线程,与此同时每个线程里面又包含了许多作业。
从图表里面我们可以看到线程2执行了一个读文件的操作,此时线程1执行了界面交互相关的代码。这个例子就是告诉你在ios中如何构建自己的代码,也就是说,在主线程里面应该都是和界面交互的工作,在第二等级的线程里面应该执行那些运行比较慢或者比较长的操作任务(例如读取文件,或者网络交互等)。
NSOperation vs. Grand Central Dispatch (GCD)
你可能听说过GCD。简单的说,GCD就是包含了很多很好的特性,动态运行时库,增强了系统在多核处理器硬件上的处理能力和对并发的支持能力。假如你想要了解更多GCD的知识,你可以看看Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.
在mac os x 10.6和ios 4之前,NSOperation 和 NSOperationQueue是不同于GCD的,并且两个使用完全不同的机制。但从在mac os x 10.6和ios 4开始之后,NSOperation 和 NSOperationQueue就是构建在GCD之上了。就一般而言,要是人们有需求,苹果推荐使用更高级别的抽象概念的时候,就会抛弃底级别的抽象概念。
这里有一些GCD 和 NSOperation,NSOperationQueue的一些区别,这样你就可以决定什么时候使用什么了:
GCD是用来呈现将要执行并发工作单元的一种轻量级的方式。你不用去安排这些工作单元,因为系统将会接管这个工作。不过增加依附于blocks可能会有一点头疼,作为一位开发者,取消或者挂起block需要一些额外的操作。
NSOperation 和 NSOperationQueue相比于GCD的话,是上升了一个等级的,你可以依附于各种各样的操作。你完全可以重用,取消或者挂起他们。而且NSOperation非常适合于KVO,例如,你可以运行一个NSOperation来监听NSNotificationCenter的消息。
初期项目规划
在初期的项目规划上,我们使用一个dictionary来作为一个table view的数据源。这个字典的key是一些图片的名字,这个对应的value就是每个图片的地址。那么目前这个项目的目标就是,读取这个dictionary的内容,然后下载这些图片,然后经过图片滤镜,最后显示在table view上面.
下面是这个项目规划示意图:
实现规划的——这应该是你首先接下来会做的
注意:假如你不想进行这个非多线程版本的项目,而是直接想看到多线程的好处,那么你可以跳过这节,在这里下载这个我们做好的项目文件。
打开xcode,创建一个空的应用程序模板(Empty Application template),命名为ClassicPhotos,选择Universal,也就是iphone、Ipad兼容模式。勾选上Use Automatic Reference Counting,其他都不勾选上了,然后保存在喜欢的地方。
然后在工程导航栏上面选择ClassicPhoto这个工程,然后在右边选择Targets\ ClassicPhotos\Build Phases, 并且展开 Link Binary with Libraries。点击+按钮,增加Core Image framework,因为我们将会用到图片滤镜。
切换到AppDelegate.h,引入ListViewController,这个将会是root view controller,后面你会声明他的,而且他也是UITableViewController子类。
#import “ListViewController.h”
|
切换到AppDelegate.m,定位到application:didFinishLaunchingWithOptions:,实例化一个ListViewController的对象,然后设置他为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或者xib文件,而是程序来创建界面。在这篇教程里面,我们就简单使用一下这样的方式。
下面就创建一个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 and Core Image,也就是import 头文件。
2、 为了方便点,我们就宏定义kDatasourceURLString这个是数据源的地址字符串。
3、 然后让ListViewController成为UITableViewController的子类,也就是替换NSObject 为 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 { // 3 self.title = @”Classic Photos”;
// 4 self.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; }
// 8 NSString *rowKey = [[self.photos allKeys] objectAtIndex:indexPath.row]; NSURL *imageURL = [NSURL URLWithString:[self.photos objectForKey:rowKey]]; NSData *imageData = [NSData dataWithContentsOfURL:imageURL]; UIImage *image = nil;
// 9 if (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、 Synthesize这个photos实例变量。
2、 这里其实是重写了photos的get函数,并且在里面实例化这个数据源对象。
3、 设置这个导航栏上的title。
4、 设置table view的行高为80.0
5、 当这个ListViewController unloaded的时候,设置photos为nil
6、 返回这个table view有多少行
7、 这个是UITableViewDelegate的可选的回调方法,然后设置每一行的高度都为80.0,其实每一行默认的44.0的高度。
8、 取得这个dictionay的key,然后得到value,就可以得到url了,然后使用nsdata来下载这个图像。
9、 加入你已经成功下载这个数据,就可以创建图像,并且可以使用深褐色的滤镜来处理一下。
10、 这个方法就是对这个图像使用深褐色的滤镜。假如你想要知道更多关于Core Image filters的知识,你可以看看Beginning Core Image in iOS 5 Tutorial.
那下面来试试。编译运行。太爽了,深褐色图像也出现了,但是似乎他们出现的有点慢。不过要是你是一边吃小吃,一边等待,你也会觉得没什么问题,非常漂亮。
线程
正如我们知道的一样,每一个app至少都有一个线程,那就是主线程。一个线程的工作就是执行一系列的指令。在Cocoa Touch里面,主线程包含了应用程序的主循环。也就是几乎所有的app的代码都是在主线程里面执行的,除非你特别的创建一个其他的线程,并且在这个新线程里面执行一些代码。
线程有两个特征:
1、 每个线程都有共同的权利来使用app的资源,不过除了局部变量。因此任何对象都可能潜在的被任何线程更改,使用。
2、 没有办法来估计一个线程将会运行多久,或者那个线程将会首先执行完。
因此,知道一些克服这些问题和避免一些不可料想的错误的技术是很重要的。下面就列举一些app将会面对的一些问题和一些关于如何高效的处理这些问题建议。
Race Condition(资源竞争):实际上每个线程都能访问同样的一块内存,所以这样就可能引起资源竞争。
当多线程并发访问这个共享数据的时候,第一个访问的这个内存的线程可能修改了这块共享数据,而且我们不能保证那个线程将会首先访问。你可能会假设用一个本地变量来保存你这个线程所写入这个共享内存的数据,但是可能就在你保存的这个时间,另外一个线程已经改变了这个值了,这样你的数据其实都已经过期了,不是最新的数据了。
假如你知道在你的代码里面会存在使用多线程来并发的读写一块数据,那么你应该使用mutex lock(互斥锁)。Mutex(互斥)就是互相排斥的意思,你可以使用“@synchronized 块”来包裹你准备使用互斥锁的实例变量。这样你就可以保证在同一个时间,只能有一个线程能够访问那块内存。
@synchronized (self) {
myClass.object = value;
}
上面代码中的self叫“semaphore”(判断信号),当一个线程执行到那段代码的时候回去检测是否其他的线程在访问自己的那段代码,假如没有线程在访问,那么他就会执行那个块里面的代码,要是有其他线程在访问,他就会等待,直到这个互斥锁变成无效的状态,也就是没人访问了。
Atomicity(单元性的):你可能在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属性都是没有线程安全的。查看官方API文档可以确认这个类是否线程安全的。假如这个API文档没有提及,那么你就应该假设他没有线程安全。
通常来说,假如你正执行在子线程里面,这个时候你要处理一些界面上的东西,使用performSelectorOnMainThread是非常好的。
Deadlock(死锁):就是一直等待一个永远也不会出现的条件,这样就会一直等待,不会进行下一步。举个例子,就像两个线程每一个都同时执行到一段代码,然后每一个线程将要等待另外一个执行完成,然后解开这个锁,但是这种情况永远也不会发生,所以这两个线程都会死锁。
Sleepy Time(未响应):这个一般是在同一时刻有太多的线程在执行,系统陷入了混乱,处理不过来了。NSOperationQueue有一个属性可以设置同时最大的并发线程数,这样就不会出现这样情况。
NSOperation API
NSOperation类声明的东西相当简短。一般通过一下步骤来创建一个定制的操作:
1. 从NSOperation中派生一个子类
2. 重写“main”函数
3. 在“main”函数中,创建一个“autoreleasepool”
4. 把你的代码放到“autoreleasepool”中。
这里创建你自己的autorelease pool的原因是因为你不应该访问主线程的autorelease pool,因此你应该自己创建一个,下面是一个例子:
#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
|
上面的例子展示了autorelease pool的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的取消方法,正如下面的:
// 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”函数,没有增加到一个队列里面去,那么这个操作将会运行在主循环里面。
Dependency(依附):你可以创建一个依附于其他操作的操作。任何操作都是可以依附于其他操作的。当你创建一个A操作依附于B操作,即使你调用了A操作的“start”函数,他也不会立即执行,除非B操作的isFinished是true,也就是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”之前,这个队列会遍历所以的操作。里面优先级高的将会先执行,如果这个操作的优先级是相同的,那么将会按照提交到队列的顺序来执行。
Completion block(完成块):NSOperation类还有另外一个有用的方法就是setCompletionBlock:。假如你想要在这个操作完成的时候做些什么,你可以把这个操作放到一个块里,然后传递给这个函数。但是注意,并不能保证这个块将会在主线程中执行。
[filterOp setCompletionBlock: ^{ NSLog(@"Finished filtering an image."); }]; |
这里有一些使用线程时额外的需要注意的地方:
1.假如你需要传入一些值和指针给这个操作,最好的方法就是自己设计一个初始化函数:
#import <Foundation/Foundation.h>
@interface MyOperation : NSOperation
-(id)initWithNumber:(NSNumber *)start string:(NSString *)string;
@end |
2.假如你的操作需要返回值或者指针,最好的方法就是声明一个delegate方法。记住啊,这个delegate方法必须在主函数中返回。然而,因为你子类化了NSOperation,所以你必须首先转换这个操作类到NSObject。就像下面这样做:
[(NSObject *)self.delegate performSelectorOnMainThread:(@selector(delegateMethod:)) withObject:object waitUntilDone:NO]; |
3.假如这个操作已经不再需要了在后台执行了,你需要经常检查isCancelled属性。
4.一般你是不需要重写“start”方法的。然而,如果你真想重写这个方法,你就不得不主要一些属性,比如isExecuting, isFinished, isConcurrent, 和 isReady。否则,你的操作将不会正确的执行。
5.一旦你增加这个操作到一个队列(NSOperationQueue的实例对象)里去,然后你释放了他(假如你没有使用ARC)。NSOperationQueue会设定拥有这个操作,也就是说会让这个操作的引用计数加一,这样就不会释放掉了, 然后就会调用这个操作的“start”函数,并且会在执行完之后释放他。
6.你不能重用一个操作。一旦你把这个操作增加到了一个队列,就算你没有了这个操作的拥有权了,就交给系统了。假如你想要使用同样一个操作,你就必须创建一个新的实例对象。
7.一个完成的操作不能被重新开始。就像你不能在结束函数里面,在让这个操作从头运行一次,这样是错误的。
8.假如你取消一个操作,这个将不会立即发生。一般是在将来的某个时刻,在“main”方法里检测到这个操作的isCancelled == YES,否则这个操作将会一直运行下去的。
9.无论一个操作是成功执行完,还是不成功执完成,或者是被取消了,isFinished的值都是会被设置成YES的。因此,决不能假设isFinished == YES就意味着一切都执行好了, 特别是假如你的代码依赖了这个isFinished,那就需要注意了。
NSOperationQueue API
NSOperationQueue的接口也是相当的简单的。甚至比NSOperation都还简单,因为你不需要子类化这个类,或者重写任何方法,你只需要简单创建一个就可以了。比较好的做法就是给你的队列名一个名字,这样你可以在运行时区分出你的操作队列,并且方便调试:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; myQueue.name = @”Download Queue”; |
1. 并发操作:队列和线程不是一回事。一个队列能够有很多的线程。队列里面的每一个操作都是执行在他自己的线程中。举个例,你创建一个队列,然后增加了三个操作到里面。这个队列将会开启三个不同的线程,然后在他们自己的线程上执行所有的操作。
有多少的线程将会被创建?这是一个很好的问题。其实主要是和硬件有关。但是一般的,NSOperationQueue类将会在场景后面做许多神奇的事情,会决定怎么样会让这个代码在这个特别平台上执行效率最高,因此会决定这个线程可能的最大数。
考虑一下的例子。假如这个系统是空闲的,并且有许多有效的资源(感觉这里资源就是,类似于内存很多,cup很空闲,可以随时进行计算),因此NSOperationQueue可能能够同时的启动8个线程。在你下一次运行这个程序的时候,这个系统可能正在忙于其他不相关的操作,这个时候NSOperationQueue就只能同时启动2个线程。
2.最大的并发操作:你可以设置NSOperationQueue能够并发执行的最大操作数。NSOperationQueue可能会选择执行任意的并发操作,但是永远不会超过设置的这个最大的数量。
myQueue.MaxConcurrentOperationCount = 3; |
假如你想设置MaxConcurrentOperationCount为默认的数量,你可以像下面这样做:
myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; |
3.增加操作: 一旦一个操作被加到一个队列里面去了,你应该通过发送释放消息给这个操作对象来解除这个拥有关系(假如你使用的是人工引用计数,没有使用ARC),接下来这个队列将会接管并且开始这个操作。所以说这个队列会决定什么时候执行“start”。
[myQueue addOperation:downloadOp]; [downloadOp release]; // manual reference counting |
4.未完成的操作:在任何时候你可以询问在这个队列里面有那些操作,一共有多少的操作。记住这一点,只有正在等待被执行和正在执行的操作可以被得到。一旦这个操作完成了,他就会从队列里面移除。
NSArray *active_and_pending_operations = myQueue.operations; NSInteger count_of_operations = myQueue.operationCount; |
5.暂停(挂起)队列:你可以通过设置setSuspended:YES来暂停一个队列。这个将会把队列里面所有的操作挂起,注意不能单独挂起操作。你只需要设置setSuspended:NO来恢复这个队列。
// Suspend a queue [myQueue setSuspended:YES]; . . . // Resume a queue [myQueue setSuspended: NO]; |
5. 取消操作:想要取消队列里面所有的操作,你只需要简单调用“cancelAllOperations”.但是你是否记起来前面我们提醒你的代码应该在NSOperation里经常的检查isCancelled的属性?
主要原因是“cancelAllOperations”效果不明显,除了在队列里面的每个操作里面调用“cancel”方法,不然效果真的不好。假如一个操作还没有开始,你调用“cancel”方法,这个操作就将会被取消,并且从这个队列里面移除。然而假如一个操作已经在执行了,那么只有这个单独的线程自己察觉取消了(也就是检查isCancelled属性),然后停止正在执行的东西。
[myQueue cancelAllOperations]; |
6. addOperationWithBlock: 假如你有一个简单的操作,而且不想子类化一个,那么你可以简单的通过block方式来传递到一个队列里面。假如你想要从这个block里面返回得到一些数据,那么请记住你不应该传递任何strong类型的指针到这个block中,相反的,你应该使用weak类型的指针。假如在这个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 operation NSURL *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 }];
}]; |
重新定义模型
是时候重新定义最初的设计的模型了。假如你仔细看过之前的模型,你应该可以看出有3个地方可以改用线程的。通过拆解这三部分,把每一个放到他们自己线程中去,这样主线程的压力就会减轻,就可以专注于交互了。
注意:假如你不能直接的观察到为什么你的app运行的如此之慢,有些时候这种情况也不是很明显,那么你应该借助一些工具。不过那就是需要另外一个教程来介绍了。
为了解决这些问题,你需要一个线程来负责交互相关的,一个线程专注于下载数据源和图片,还有一个线程来执行图片滤镜。在这个新的模型里面,这个app在主线程里面启动,加载一个空的table view。在这个时候,这个app启动了第二个线程来下载数据源。
一旦这个数据源被下载下来之后,你应该告诉table view来重新加载数据。这些都是在主线程中完成的。这个时刻,table view知道有多少行,而且也知道需要展示图片的ur,但是他却没有这个真正的图片数据。假如这个时候你直接开始现在这个图片,这将会非常糟糕的决定,其实这个时候你并不需要所有的图片的。
那么怎么做才是比较好的喃?
一个比较好的方式是只开始下载将会显示在屏幕上的图片。因此你应该首先询问这个table view那些行是可见的,然后才是那些可见的行才开始这个下载过程了。正如前面讨论的,这个图片滤镜处理应该在这个图片被下载完后,而且还没有被处理,才能开始这个图片处理过程。
为了让这个app有更好的响应方式,这个代码应该可以先展示这个没有处理的图片。一旦这个图片滤镜处理完成,就可以更新到这个UI上了。下面的图表展示了这个整个原理的流程。
为了完成这个目标,你需要跟踪这些操作,是否正在下载图片,是否已经下载完成,是否已经处理完图片滤镜了。你需要跟踪这些每一步操作,看他在下载或者处理滤镜的状态,这样你可以在滑动界面的时候,取消,暂停或者恢复这些操作。
Okay,现在我们开始编码!
打开之前留下的那个工程,增加一个NSObject的子类,名字为PhotoRecord的类。打开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 |
上面的语法看起来是否熟悉?每一个属性都有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 |
为了跟踪每一个操作的状态,你需要另外一个类,所以从NSObject派生一个名为PendingOperations的子类。切换到这个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的每一行的indexPath有关系,字典的value将会分别是ImageDownloader 和 ImageFiltration的实例对象。
注意:你可以想要直到为什么不得不跟踪这个操作的活动和完成的状态。难道不可以简单的通过在[NSOperationQueue operations]中查询这些操作得到这些数据么?答案是当然可以的,不过在这个工程中没有必要这样做。
每个时候你需要可见行的indexPath同所有行的indexPath的比较,来得到这个完成的操作,这样你将需要很多迭代循环,这些操作都很费cpu的。通过声明了一个额外的字典对象,你可以方便的跟踪这些操作,而且不需要这些无用的循环操作。
切换到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方法,这样直到他们被访问的时候才会实例化他们。这里我们也实例化两个队列,一个是下载的操作,一个滤镜的操作,并且设置了他们的一些属性,以至于你在其他类中访问这些变量的时候,不用去关心他们初始化。在这篇教程里面,我们设置了maxConcurrentOperationCount为1.
现在是时候关心下载和滤镜的操作了。创建一个NSOperation的子类,名叫ImageDownloader。切换到ImageDownloader.h,,增加下面的:
#import <Foundation/Foundation.h>
// 1 #import “PhotoRecord.h”
// 2 @protocol ImageDownloaderDelegate;
@interface ImageDownloader : NSOperation
@property (nonatomic, assign) id <ImageDownloaderDelegate> delegate;
// 3 @property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readonly, strong) PhotoRecord *photoRecord;
// 4 - (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>) theDelegate;
@end
@protocol ImageDownloaderDelegate <NSObject>
// 5 - (void)imageDownloaderDidFinish:(ImageDownloader *)downloader; @end |
下面解释一下上面相应编号处的代码的意思:
1. 引入PhotoRecord.h,这样当下载完成的时候,你可以直接设置这个PhotoRecord的图像属性。假如下载失败了,可以设置失败的值为yes。
2. 声明一个delegate,这样一旦这个操作完成,你可以通知这个调用者。
3. 声明一个indexPathInTableView,这样你可以方便的直到调用者想要操作哪里行。
4. 声明一个特定的初始化方法。
5. 在你的delegate方法里面,你传递了整个这个类给调用者,这样调用者可以访问indexPathInTableView 和 photoRecor。因为你需要转换这个操作为一个对象,并且返回到主线程中,而且这里这样做有个好处,就是只用返回一个变量。
Switch to ImageDownloader.m and make the following changes:
切换到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]) { // 2 self.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、 声明一个私有接口,这样你可以更改这个实例变量的属性为读和写。
2、 设置属性
3、 有计划的去检查isCancelled,这样可以确保你尽可能随时可以终止这个操作。
4、 苹果建议使用@autoreleasepool块来代替alloc和初始化NSAutoreleasePool,因为使用block有更高的效率。你也完全可以使用NSAutoreleasePool来代替的,这样也是很好的。
5、 强制转换为NSObject对象,并且在主线程中通知这个调用者。
现在继续创建一个NSOperation的子类来负责图像滤镜的功能!
创建一个NSOperation的子类,名为ImageFiltration,打开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 frameworks。你也需要导入PhotoRecord。就像前面的ImageDownloader一样,你想要调用者使用我们定制的初始化方法。
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 consuming CIImage *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 consuming CGImageRef 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,一个好的编程习惯是一般在一个很消耗cpu的操作前调用这个检查,可以避免一些不必要的消耗。一旦这个图像滤镜完成,PhotoRecord的实例变量的值就在适当的时候设置为这个新的,并且还需要通知主线程,完成了。
太好了!现在你已经有了所有的在后台操作的工具和一些基础了。是时候回到view controller了,并且适当的修改一下,这样你就可以利用这个新的特性了。
注意:在继续进行工程前,你需要到GitHub去下载AFNetworking库。
AFNetworking是构架于NSOperation 和 NSOperationQueue之上的。他提供了许多很方便的方法用于在后台下载。苹果也提供了NSURLConnection,这个也可以用于我们下载这个记录了所有图片的一张表的操作,但是你完全没有必要为了这个表来做一些额外的工作,所以直接使用AFNetworking是很方便的。你只需要传递两个block进来就可以了,一个是当下载成功完成的时候,一个是当操作失败的时候,后面会给你详细说明的。
现在把这个库增加到你的工程中,选择File > Add Files To …,然后选择到你下载下来的AFNetworking,然后点击“Add”。这里你要确定勾选了“Copy items into destination group’s folder”。这里我们使用了ARC的,目前最新的AFNetworking已经支持ARC了,要是你使用的是以前手动管理内存的方法,你需要做一些更改,不然会有很多错误的。
在左上角点击导航栏下面点击“PhotoRecords”。然后在右边,在“Targets”下面选择“ClassicPhotos”。然后选择“Build Phases”,在这个下面,展开“Compile Sources”。选择所有属于AFNetworking的文件,然后点击Enter,弹出一个对话框,在这个对话框里面输入“fno-objc-arc”,点击“Done”完成。其实在AFNetworking的Github上,这个说明很清楚的,可以去看看。
切换到ListViewController.m,并且做下面的修改: // 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、 在这个类里面,我们不需要CoreImage了,所以删除他的头文件,但是我们需要导入PhotoRecord.h, PendingOperations.h, ImageDownloader.h 和 ImageFiltration.h。
2、 这里涉及到了AFNetworking库
3、 确保ListViewController包含了ImageDownloader 和 ImageFiltration delegate的方法。
4、 这里你已经不再需要数据源了。你将会创建一个使用图片属性表的PhotoRecord的实例对象。所以,你应该把“photos”从NSDictionary 变为 NSMutableArray,这样你可以更新图片数组。
5、 这个属性用于跟踪挂起的操作的。
切换到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) {
// 1 NSURL *datasourceURL = [NSURL URLWithString:kDatasourceURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:datasourceURL];
// 2 AFHTTPRequestOperation *datasource_download_operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
// 3 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
// 4 [datasource_download_operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// 5 NSData *datasource_data = (NSData *)responseObject; CFPropertyListRef plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, (__bridge CFDataRef)datasource_data, kCFPropertyListImmutable, NULL);
NSDictionary *datasource_dictionary = (__bridge NSDictionary *)plist;
// 6 NSMutableArray *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; }
// 7 self.photos = records;
CFRelease(plist);
[self.tableView reloadData]; [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
// 8 // Connection error message UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@”Oops!” message:error.localizedDescription delegate:nil cancelButtonTitle:@”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类,新建并且使用了一个请求来初始化。
3、 给用户反馈,当在下载这个数据的时候,激活网络活动指示器。
4、 通过使用setCompletionBlockWithSuccess:failure:方法,我们可以增加两个block:一个是成功的,一个是失败的。
5、 在成功的block里面,下载的这个图片属性表转化NSData,然后使用toll-free bridging(core foundation 和foundation之间的数据桥接,也就是c和objc的桥接)来转化这个数据到CFDataRef 和 CFPropertyList,接着转化到NSDictionary。
6、 新建一个NSMutableArray对象,遍历这个字典的所有对象和key,通过这些对象和key新建一些PhotoRecord的实例对象,并且储存在这个数组里面。
7、 一旦遍历完成,让这个_photo变量指向这个记录数组,并且从新加载table view,还需要停止网络活动指示器。这里你需要释放掉“plist”这个实例变量。
8、 在这个失败的block里面,我们将展示一个消息来通知用户。
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;
// 1 UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; cell.accessoryView = activityIndicatorView;
}
// 2 PhotoRecord *aRecord = [self.photos objectAtIndex:indexPath.row];
// 3 if (aRecord.hasImage) {
[((UIActivityIndicatorView *)cell.accessoryView) stopAnimating]; cell.imageView.image = aRecord.image; cell.textLabel.text = aRecord.name;
} // 4 else if (aRecord.isFailed) { [((UIActivityIndicatorView *)cell.accessoryView) stopAnimating]; cell.imageView.image = [UIImage imageNamed:@"Failed.png"]; cell.textLabel.text = @”Failed to load”;
} // 5 else {
[((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的实例对象中。通过indexpath里面的row来得到每一个数据。
3、 检查这个PhotoRecord。假如这个图像被下载下来了,就显示这个图片,显示图片的名字,还要停止这个活动指示器。
4、 假如下载图像失败了,就在显示一个失败的图片,来告诉用户下载失败了,并且要停止这个活动指示器。
5、 假如这个图片还没有被下载下来。就开始下载和滤镜的操作(他们还没有实现),这个时候显示一个占位的图片和激活活动指示器来提醒用户正在工作。
现在是时候实现我们一直关注的开始操作的方法了。假如你还没有准备好,你可以在ListViewController.m中删除“applySepiaFilterToImage:”的以前实现方式。
到最下面的代码的地方,实现下面的方法: // 1 - (void)startOperationsForPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {
// 2 if (!record.hasImage) { // 3 [self startImageDownloadingForRecord:record atIndexPath:indexPath];
}
if (!record.isFiltered) { [self startImageFiltrationForRecord:record atIndexPath:indexPath]; } } |
上面的代码相当直接,但是也解释一下上面做的:
1、 为了简单,我们传递需要被操作的PhotoRecord的实例对象,顺带和对应的indexpath。
2、 检查一下是否已经有图片了。假如有,就可以忽略那个方法了。
3、 假如还没有图片,就调用startImageDownloadingForRecord:atIndexPath:来开始下载图片的操作(后面将会简单实现)。图片滤镜也是这样操作的,假如还没有进行图片过滤,就调用startImageFiltrationForRecord:atIndexPath:来过滤图片(后面将会简单实现)。
注意:这里把图像下载和滤镜分开是有原因的,假如这个图片被下载下来了,但是用户滑动了,这个图片就看不到了,我们将不会进行图片滤镜处理。但是下一次他又滑动回来了,这个时候,图片我们已经有了,就只需要进行图片滤镜的操作了。这样会更有效率的。
现在我们需要实现上面一小段带面里面的startImageDownloadingForRecord:atIndexPath:方法了。我们之前创建一个跟踪操作的一个类,PendingOperations,这里我们就会用到他:
- (void)startImageDownloadingForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath { // 1 if (![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPath]) {
// 2 // Start downloading ImageDownloader *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 { // 3 if (![self.pendingOperations.filtrationsInProgress.allKeys containsObject:indexPath]) {
// 4 // Start filtration ImageFiltration *imageFiltration = [[ImageFiltration alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self];
// 5 ImageDownloader *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的实例对象,并且设置他的delegate为ListViewController。我们还会传递这个indexpath和PhotoRecord的实例变量,然后把这个实例对象增加到下载队列。你也需要把他增加到downloadsInProgress里面去保持跟踪。
3、 相似的,也这样去检查图片是否被过滤了。
4、 假如没有被过滤,那么也就初始化一个。
5、 这里有一点考虑的,你必须检查是否这个indexpath对应的已经被挂起来下载了,假如是这样的,那么就使你的过滤操作依附于他。否则就可以不依附了。
太好了。你现在需要实现这个delegate的ImageDownloader 和 ImageFiltration方法了。把下面这些增加到ListViewController.m中去:
- (void)imageDownloaderDidFinish:(ImageDownloader *)downloader {
// 1 NSIndexPath *indexPath = downloader.indexPathInTableView; // 2 PhotoRecord *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、 得到这个操作的indexpath,无论他是下载还是滤镜的。
2、 得到PhotoRecord的实例
3、 更新UI
4、 从downloadsInProgress (或者 filtrationsInProgress)里面移除这个操作。
更新:为了能够更好的掌控PhotoRecord。因为你传递PhotoRecord的指针到ImageDownloader 和 ImageFiltration中,你可以随时直接修改的。因此,使用replaceObjectAtIndex:withObject:方法来更新数据源。详情见最终的工程。
酷哦!
Wow!我们成功了!编译运行,你现在操作一下,发现这个app都不卡,并且下载图片和图片滤镜都可用的。
难道这个还不酷么?我们还可以做点改变,这样我们的app可以有更好的人机交互和性能。
简单调整
我们已经经历了一个很长的教程!现在的工程相比以前的已经做了很多的改变了。但是有一个细节我们还没有注意到。我们是想要成为一个伟大的程序员,而不是一个好的程序员。所以我们应该改掉这个。你可能已经注意到了,当我们滚动这个table view的时候,下载和图片滤镜依然在运行。所以我们应该在滑动的时候取消这些东西。
回到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 不滚动的时候才开始操作。这些其实UIScrollView的属性,但是UITableView是继承至UIScrollView,所以他也就自动继承了这些属性。
现在到ListViewController.m的最下面,实现下面的UIScrollView的delegate方法:
#pragma mark - #pragma mark – UIScrollView delegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // 1 [self suspendAllOperations]; }
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { // 2 if (!decelerate) { [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } }
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // 3 [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } |
快速看看上面代码展示了什么:
1、 一旦用户开始滑动了,你就应将所有的操作挂起。后面将会实现suspendAllOperations方法。
2、 假如decelerate的值是NO,那就意味着用户停止拖动这个table view了。因此你想要恢复挂起的操作,取消那些屏幕外面的cell的操作,开始屏幕内的cell的操作。后面将会实现loadImagesForOnscreenCells 和 resumeAllOperations的方法。
3、 这个delegate方法是告诉你table view停止滚动了,所做的和第二步一样的做法。
现在就来实现suspendAllOperations, resumeAllOperations, loadImagesForOnscreenCells方法,把下面的加到ListViewController.m的下面:
#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 {
// 1 NSSet *visibleRows = [NSSet setWithArray:[self.tableView indexPathsForVisibleRows]];
// 2 NSMutableSet *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];
// 5 for (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;
// 6 for (NSIndexPath *anIndexPath in toBeStarted) {
PhotoRecord *recordToProcess = [self.photos objectAtIndex:anIndexPath.row]; [self startOperationsForPhotoRecord:recordToProcess atIndexPath:anIndexPath]; } toBeStarted = nil;
} |
suspendAllOperations, resumeAllOperations 和 cancelAllOperations都是一些简单的实现。你一般想要使用工厂方法来挂起,恢复或者取消这些操作和队列。但是为了方便,把他们放到每一个单独的方法里面。
LoadImagesForOnscreenCells是有一点复杂,下面就解释一下:
1、 得到可见的行
2、 得到所有挂起的操作(包括下载和图片滤镜的)
3、 得到需要被操作的行 = 可见的 – 挂起的
4、 得到需要被取消的行 = 挂起的 – 可见的
5、 遍历需要取消的,取消他们,并且从PendingOperations里面移除。
6、 遍历需要被开始,每一个调用startOperationsForPhotoRecord:atIndexPath:方法。
最后一个需要解决的就是解决ListViewController.m中的didReceiveMemoryWarning方法。
// If app receive memory warning, cancel all operations - (void)didReceiveMemoryWarning { [self cancelAllOperations]; [super didReceiveMemoryWarning]; } |
编译运行,你应该有一个更好的响应,更好的资源管理程序了。慢慢欢呼吧!
何去何从?
这里是这个完整的工程。
假如你完成了这个工程,并且花了一些时间懂得这些,那么祝贺你!你已经比开始这个教程的时候懂得了很多。要想完全懂得这些东西,你需要了解和做很多工作的。线程其实也是有些微的bug,,但是一般都不容易出现,可能会在网络非常慢,代码运行在很快或者很慢的设备,或者在多核设备上出现bug。测试需要非常的仔细,并且一般需要借助工具或者你观察来核查这个线程来做一些修改。