这篇文章还可以在这里找到 英语
以下是对上面代码的注解:
切换到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 |
通过代码注释,你将看到上面的代码在做下面的操作:
现在,继续创建一个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 |
再次的,以下是对上面代码的注释:
切换到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参数;在任何系统资源消耗较大的函数调用前后去调用这个 滤镜处理函数,是不错的做法。一旦滤镜处理结束了,PhotoRecord实例的值会被恰当的设置好,然后主线程的delegate被通知了。
很好!现在你已经有了在后台线程中执行操作(operations)的所有工具和基础了。是时候回到view controller然后恰当的修改它,以便它可以利用好这些新优势。
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 |
这里发生了什么事?以下要点对上面的代码做了解释:
切换到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; } |
以上代码做了一些操作。下面的内容是对代码完成内容的一步步解析:
来到 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; } |
同样的,花点时间看下下面的评论解析:
现在是时候来实现负责启动操作的方法了。如果你还没有实现它,可以在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]; } } |
以上的代码相当直接,但是有些东西要解释下:
现在你需要去实现以上代码段的startImageDownloadingForRecord:atIndexPath:方法。记住你创建了一个自定义的类,PendingOperations,用于检测操作(operations)。在这里你开始使用它了。
- (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]; } } |
好的!以下是简短的解析,以确保你理解了以上代码的工作原理。
很好!你现在需要去实现ImageDownloader和ImageFiltration的delegate方法了。将下列代码添加到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方法都有非常相似的实现,所以这里只需要拿其中一个做讲解:
更新:关于处理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 { // 2 if (!decelerate) { [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // 3 [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } |
以下是对上面代码的解析:
好的!现在,在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 { // 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方法有点复杂。以下是对它的解释:
最后,这个难题的最后项由ListViewController.m文件中的didReceiveMemoryWarning方法解决。
// If app receive memory warning, cancel all operations - (void)didReceiveMemoryWarning { [self cancelAllOperations]; [super didReceiveMemoryWarning]; } |
编译运行工程,你会看到一个响应更加灵敏,有更好的资源管理的应用程序!给自己一点掌声吧!
这里是工程改进后的完整代码。
如果你完成了这个工程,并且花时间真正理解了它,恭喜!相比刚阅读本教程时,你可以把自己看待成一个更有价值的iOS开发者了!大部分的开发工作室都会幸运的拥有一两个能真正理解这些原理的人。
但是注意 — 像deeply-nested blocks(块),无理由地使用线程会让维护你的代码的人难以理解。线程会引来不易察觉的bugs,只有当网络缓慢时才会出现,或者当代码运行在一个更 快(或者更慢)的设备中,或者有不同内核数目的设备中。仔细认真的测试,经常使用Instruments(或者是你自己的观察)来核实引入的线程是否真的 取得了性能提升。
如果你对本教程或者NSOperations有任何的意见或者问题,请加入下面的论坛讨论!
作者:
这篇blog是由一位个人iOS开发者 Soheil Moayedi Azarpour 发表的。
翻译者:
欧泽林目前在ZAKER,一款中国最流行的新闻阅读类app,负责iOS端的开发工作,在中国广州。他是位苹果的超级粉丝,曾经以学生身份参加过 2011年6月份的苹果全球开发者大会(WWDC)。刚毕业不久,喜欢与苹果有关的一切东西,希望可以跟更多人分享交流。你可以在Twitter, Facebook, 或者 Weibo上找到他。