如何使用NSOperations和NSOperationQueues 第二部分

这篇文章还可以在这里找到 英语

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

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

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

// 1
@interface ImageDownloader ()
@property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readwrite, strong) PhotoRecord *photoRecord;
@end
 
 
@implementation ImageDownloader
@synthesize delegate = _delegate;
@synthesize indexPathInTableView = _indexPathInTableView;
@synthesize photoRecord = _photoRecord;
 
#pragma mark -
#pragma mark - Life Cycle
 
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>)theDelegate {
 
    if (self = [super init]) {
        // 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. 申明一个私有的接口,你可以改变实例变量的属性为读写(read-write)。
  2. 设定properties属性。
  3. 经常检查isCancelled变量,确保操作即时结束。
  4. 苹果推荐使用@autoreleasepool块,而不是alloc和init NSAutoreleasePool变量,因为blocks更加有效率。你也许会使用NSAutoreleasePool,这样也行。
  5. 将operation对象强制转换为NSobject类型,然后在主线程中通知调用者(caller)。

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

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

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

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

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

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

 
@interface ImageFiltration ()
@property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView;
@property (nonatomic, readwrite, strong) PhotoRecord *photoRecord;
@end
 
 
@implementation ImageFiltration
@synthesize indexPathInTableView = _indexPathInTableView;
@synthesize photoRecord = _photoRecord;
@synthesize delegate = _delegate;
 
#pragma mark -
#pragma mark - Life cycle
 
- (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate {
 
    if (self = [super init]) {
        self.photoRecord = record;
        self.indexPathInTableView = indexPath;
        self.delegate = theDelegate;
    }
    return self;
}
 
 
#pragma mark -
#pragma mark - Main operation
 
 
- (void)main {
    @autoreleasepool {
 
        if (self.isCancelled)
            return;
 
        if (!self.photoRecord.hasImage)
            return;
 
        UIImage *rawImage = self.photoRecord.image;
        UIImage *processedImage = [self applySepiaFilterToImage:rawImage];
 
        if (self.isCancelled)
            return;
 
        if (processedImage) {
            self.photoRecord.image = processedImage;
            self.photoRecord.filtered = YES;
            [(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageFiltrationDidFinish:) withObject:self waitUntilDone:NO];
        }
    }
 
}
 
#pragma mark -
#pragma mark - Filtering image
 
 
- (UIImage *)applySepiaFilterToImage:(UIImage *)image {
 
    // This is expensive + time 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 library from GitHub.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    static NSString *kCellIdentifier = @"Cell Identifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
 
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
 
        // 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的所有实例。根据行(row)的indexPath参数,从photos数组中获取并创建相应的PhotoRecord实例。
  3. 检查PhotoRecord。看它的图片是否已经下载完了,显示了图片,图片的名字,然后停止了活动指示器 (activity indicator)。
  4. 如果下载图片失败,显示一个预留图片来提示失败情况,然后停止活动指示器(activity indicator)。
  5. 否则,图片还没有被下载下来。开始下载和图片滤镜处理操作(它们现在还没有被实现),然后显示一个预留图片表示你正在对它进行处理。启动活动指示器(activity indicator)来提醒用户有操作正在进行。

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

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

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

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

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

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

- (void)startImageDownloadingForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath {
    // 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的实例,然后设置ListViewController作为它的 delegate。传入恰当的indexPath和一个指针给PhotoRecord的实例,然后把它添加到下载队列中。你还要把它添加到 downloadsInProgress中,来帮助监测事情。
  3. 同样的,检查看是否有任何的滤镜处理操作在特定的indexPath项中进行。
  4. 如果没有,使用指定的初始化函数开始一个。
  5. 这里的代码有点巧妙。你首先必须检查看这个特定的indexPath项是否有一个等待的下载任务;如果是,你可以基于该特定项创建这个滤镜操作。

很好!你现在需要去实现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方法都有非常相似的实现,所以这里只需要拿其中一个做讲解:

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

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

赞!

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

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

进一步地调整

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

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

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

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

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

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

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

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

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

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

 
#pragma mark - 
#pragma mark - Cancelling, suspending, resuming queues / operations
 
- (void)suspendAllOperations {
    [self.pendingOperations.downloadQueue setSuspended:YES];
    [self.pendingOperations.filtrationQueue setSuspended:YES];
}
 
 
- (void)resumeAllOperations {
    [self.pendingOperations.downloadQueue setSuspended:NO];
    [self.pendingOperations.filtrationQueue setSuspended:NO];
}
 
 
- (void)cancelAllOperations {
    [self.pendingOperations.downloadQueue cancelAllOperations];
    [self.pendingOperations.filtrationQueue cancelAllOperations];
}
 
 
- (void)loadImagesForOnscreenCells {
 
    // 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. 获取一NSSet可见行(rows)。
  2. 获取一NSMutableSet所有的等待操作(下载和滤镜处理)。
  3. Rows(或者indexPaths)对应的开始操作(operation),等于visible rows — pendings的数量。
  4. Rows(或者indexPaths)对应的需要取消的操作,等于pendings — visible rows的数量。
  5. 循环查看需要被取消的操作,取消它们,然后从PendingOperations中移除它们的引用。
  6. 循环查看需要开始的操作,为它们中的每一个调用startOperationsForPhotoRecord:atIndexPath:方法。

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

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

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

ClassicPhotos-improved

ClassicPhotos (改进版本)

现在还可以做什么?

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

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

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

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

作者:

这篇blog是由一位个人iOS开发者 Soheil Moayedi Azarpour 发表的。

翻译者:

Picture of Oliver Ou

欧泽林目前在ZAKER,一款中国最流行的新闻阅读类app,负责iOS端的开发工作,在中国广州。他是位苹果的超级粉丝,曾经以学生身份参加过 2011年6月份的苹果全球开发者大会(WWDC)。刚毕业不久,喜欢与苹果有关的一切东西,希望可以跟更多人分享交流。你可以在Twitter, Facebook, 或者 Weibo上找到他。

posted @ 2013-11-25 09:53  如来藏  阅读(366)  评论(0编辑  收藏  举报