原文地址:html
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueuesios
本文由 大侠自来也(泰然教程组) 翻译,转载请注明出处!!!git
每一个人应该都有使用某款ios或者mac的app的时候出现未响应的现象吧。若是是mac下面的app,要是比较幸运的话,那还会出现无敌风火轮,直到你可以操做才消失。 若是是ios的app,就只能等了,有些时候还可能就这样卡闪退了,这样就会给用户不好的用户体验。程序员
解释这个现象却是很简单:就是你的app须要一些消耗大量cpu计算时间的任务的时候,在主线程里面就基本上没时间来处理你的UI交互了,因此看起来就卡了。github
通常的一个解决办法就是经过并发处理来让当前复杂的计算离开当前的主线程,也就是说使用多线程来执行你的任务。这样的话,用户交互就会有反应,不会出现卡的状况。面试
还有一种在ios中并发处理的方法就是使用NSOperation和NSOperationQueue。在这篇教程里面,你将会学习如何使用他们。为了看到他的效果,首先咱们会建立一个一点也不使用多线程的app,因此你将会看到这个app运行时是如此的不流畅,交互性如此的很差。而后咱们会重写这个app,这个时候会加上并发处理,会给你提供良好的人机交互感觉。编程
在开始这篇教程的时候,要是你去读一下ios官方的多线程和GCD教程,会对你有很大的帮助的。不过这篇教程是比较简单的,因此你也能够不用去读刚刚的那个教程,不过建议去看看,很好的。数组
背景xcode
在开始这篇教程的时候,有一些技术概念须要普及一下。你应该据说过并发处理和并行处理。从技术点上来看,并发是程序的性质,并行是硬件的性质。因此并行和并发实际上是两个不一样的概念。做为一个程序员,你永远不能保证你的代码将会运行在一台可以使用并行处理的的机器上。可是你能够设计你的代码以致于你可使用并发处理。(这里简单用一个比喻来讲明一下并发和并行。并发就是:假若有三我的,每一个人一个水果,可是在他们面前只有一把水果刀,因而每一个人都会轮流来使用这把刀来削水果。并行就是:有三把水果刀了,每一个人均可以干本身的事,而不用去等待别人。因此并行效率会很高,可是受硬件限制,并发其实就是多线程)。安全
首先,知道一些专业术语是很重要的:
做业: 一些须要被处理的简单的工做。
线程: 在一个应用程序里,由操做系统提供的,包含了不少可执行指令的集合。
进程: 一块可执行的二进制代码,由许多线程组成。
注意: 在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 |
如今让咱们来看看上面代码的意思吧:
一、 引入UIKit and Core Image,也就是import 头文件。
二、 为了方便点,咱们就宏定义kDatasourceURLString这个是数据源的地址字符串。
三、 而后让ListViewController成为UITableViewController的子类,也就是替换NSObject 为 UITableViewController。
四、 声明一个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 |
上面增长了不少代码,不要惊慌,咱们这就来解释一下:
一、 Synthesize这个photos实例变量。
二、 这里实际上是重写了photos的get函数,而且在里面实例化这个数据源对象。
三、 设置这个导航栏上的title。
四、 设置table view的行高为80.0
五、 当这个ListViewController unloaded的时候,设置photos为nil
六、 返回这个table view有多少行
七、 这个是UITableViewDelegate的可选的回调方法,而后设置每一行的高度都为80.0,其实每一行默认的44.0的高度。
八、 取得这个dictionay的key,而后获得value,就能够获得url了,而后使用nsdata来下载这个图像。
九、 加入你已经成功下载这个数据,就能够建立图像,而且可使用深褐色的滤镜来处理一下。
十、 这个方法就是对这个图像使用深褐色的滤镜。假如你想要知道更多关于Core Image filters的知识,你能够看看Beginning Core Image in iOS 5 Tutorial.
那下面来试试。编译运行。太爽了,深褐色图像也出现了,可是彷佛他们出现的有点慢。不过要是你是一边吃小吃,一边等待,你也会以为没什么问题,很是漂亮。
线程
正如咱们知道的同样,每个app至少都有一个线程,那就是主线程。一个线程的工做就是执行一系列的指令。在Cocoa Touch里面,主线程包含了应用程序的主循环。也就是几乎全部的app的代码都是在主线程里面执行的,除非你特别的建立一个其余的线程,而且在这个新线程里面执行一些代码。
线程有两个特征:
一、 每一个线程都有共同的权利来使用app的资源,不过除了局部变量。所以任何对象均可能潜在的被任何线程更改,使用。
二、 没有办法来估计一个线程将会运行多久,或者那个线程将会首先执行完。
所以,知道一些克服这些问题和避免一些不可料想的错误的技术是很重要的。下面就列举一些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 |
解释一下上面数字注释的地方:
一、 声明一个私有接口,这样你能够更改这个实例变量的属性为读和写。
二、 设置属性
三、 有计划的去检查isCancelled,这样能够确保你尽量随时能够终止这个操做。
四、 苹果建议使用@autoreleasepool块来代替alloc和初始化NSAutoreleasePool,由于使用block有更高的效率。你也彻底可使用NSAutoreleasePool来代替的,这样也是很好的。
五、 强制转换为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 |
又来解释一下代码:
一、 因为你须要对UIImage实例对象直接操做图片滤镜,因此你须要导入UIKit和CoreImage frameworks。你也须要导入PhotoRecord。就像前面的ImageDownloader同样,你想要调用者使用咱们定制的初始化方法。
二、 声明一个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 |
下面又开始解释代码:
一、 在这个类里面,咱们不须要CoreImage了,因此删除他的头文件,可是咱们须要导入PhotoRecord.h, PendingOperations.h, ImageDownloader.h 和 ImageFiltration.h。
二、 这里涉及到了AFNetworking库
三、 确保ListViewController包含了ImageDownloader 和 ImageFiltration delegate的方法。
四、 这里你已经再也不须要数据源了。你将会建立一个使用图片属性表的PhotoRecord的实例对象。因此,你应该把“photos”从NSDictionary 变为 NSMutableArray,这样你能够更新图片数组。
五、 这个属性用于跟踪挂起的操做的。
切换到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; } |
上面的信息量有点大,咱们一步一步的来解释:
一、 建立一个NSURL 和一个 NSURLRequest来指向数据的地址。
二、 使用到了AFHTTPRequestOperation类,新建而且使用了一个请求来初始化。
三、 给用户反馈,当在下载这个数据的时候,激活网络活动指示器。
四、 经过使用setCompletionBlockWithSuccess:failure:方法,咱们能够增长两个block:一个是成功的,一个是失败的。
五、 在成功的block里面,下载的这个图片属性表转化NSData,而后使用toll-free bridging(core foundation 和foundation之间的数据桥接,也就是c和objc的桥接)来转化这个数据到CFDataRef 和 CFPropertyList,接着转化到NSDictionary。
六、 新建一个NSMutableArray对象,遍历这个字典的全部对象和key,经过这些对象和key新建一些PhotoRecord的实例对象,而且储存在这个数组里面。
七、 一旦遍历完成,让这个_photo变量指向这个记录数组,而且重新加载table view,还须要中止网络活动指示器。这里你须要释放掉“plist”这个实例变量。
八、 在这个失败的block里面,咱们将展现一个消息来通知用户。
九、 最后,增长“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; } |
看看上面代码,如今来解释一下:
一、 建立了一个UIActivityIndicatorView,而且设置他为这个cell的accessory view,用来提供一个反馈给用户。
二、 数据源包含在PhotoRecord的实例对象中。经过indexpath里面的row来获得每个数据。
三、 检查这个PhotoRecord。假如这个图像被下载下来了,就显示这个图片,显示图片的名字,还要中止这个活动指示器。
四、 假以下载图像失败了,就在显示一个失败的图片,来告诉用户下载失败了,而且要中止这个活动指示器。
五、 假如这个图片尚未被下载下来。就开始下载和滤镜的操做(他们尚未实现),这个时候显示一个占位的图片和激活活动指示器来提醒用户正在工做。
如今是时候实现咱们一直关注的开始操做的方法了。假如你尚未准备好,你能够在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]; } } |
上面的代码至关直接,可是也解释一下上面作的:
一、 为了简单,咱们传递须要被操做的PhotoRecord的实例对象,顺带和对应的indexpath。
二、 检查一下是否已经有图片了。假若有,就能够忽略那个方法了。
三、 假如尚未图片,就调用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]; } } |
下面就解释一下上面代码作了什么,确保你可以懂得。
一、 首先,检查一下这个indexpath的操做是否已经在downloadsInProgress里面了。假如在就能够忽略掉。
二、 假如没有在,就建立一个ImageDownloader的实例对象,而且设置他的delegate为ListViewController。咱们还会传递这个indexpath和PhotoRecord的实例变量,而后把这个实例对象增长到下载队列。你也须要把他增长到downloadsInProgress里面去保持跟踪。
三、 类似的,也这样去检查图片是否被过滤了。
四、 假如没有被过滤,那么也就初始化一个。
五、 这里有一点考虑的,你必须检查是否这个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 方法实现方法很是类似,所以就解释其中一个就能够了:
一、 获得这个操做的indexpath,不管他是下载仍是滤镜的。
二、 获得PhotoRecord的实例
三、 更新UI
四、 从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]; } |
快速看看上面代码展现了什么:
一、 一旦用户开始滑动了,你就应将全部的操做挂起。后面将会实现suspendAllOperations方法。
二、 假如decelerate的值是NO,那就意味着用户中止拖动这个table view了。所以你想要恢复挂起的操做,取消那些屏幕外面的cell的操做,开始屏幕内的cell的操做。后面将会实现loadImagesForOnscreenCells 和 resumeAllOperations的方法。
三、 这个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是有一点复杂,下面就解释一下:
一、 获得可见的行
二、 获得全部挂起的操做(包括下载和图片滤镜的)
三、 获得须要被操做的行 = 可见的 – 挂起的
四、 获得须要被取消的行 = 挂起的 – 可见的
五、 遍历须要取消的,取消他们,而且从PendingOperations里面移除。
六、 遍历须要被开始,每个调用startOperationsForPhotoRecord:atIndexPath:方法。
最后一个须要解决的就是解决ListViewController.m中的didReceiveMemoryWarning方法。
// If app receive memory warning, cancel all operations - (void)didReceiveMemoryWarning { [self cancelAllOperations]; [super didReceiveMemoryWarning]; } |
编译运行,你应该有一个更好的响应,更好的资源管理程序了。慢慢欢呼吧!
何去何从?
这里是这个完整的工程。
假如你完成了这个工程,而且花了一些时间懂得这些,那么祝贺你!你已经比开始这个教程的时候懂得了不少。要想彻底懂得这些东西,你须要了解和作不少工做的。线程其实也是有些微的bug,,可是通常都不容易出现,可能会在网络很是慢,代码运行在很快或者很慢的设备,或者在多核设备上出现bug。测试须要很是的仔细,而且通常须要借助工具或者你观察来核查这个线程来作一些修改。