"每一个应用程序至少有一个主线程。线程的工作就是去执行一系列的指令。在Cocoa Touch中,主线程包含应用程序的主运行回路。几乎所有你写的代码都会在主线程中执行,除非你特别创建"
线程
每一个应用程序至少有一个主线程。线程的工作就是去执行一系列的指令。在Cocoa Touch中,主线程包含应用程序的主运行回路。几乎所有你写的代码都会在主线程中执行,除非你特别创建了一个单独的线程,并在这个新线程中执行代码。
线程有两个显著的特征:
1.每个线程都有访问你的应用程序资源的同等权限;它包括访问除了局部变量之外的所有的对象。所以,任何对象都可能被任意线程修改,使用并且改变。
2.没有办法可以去预测一个线程会运行多久—或者哪个线程会首先完成!
所以,知道这些技术很重要,它们可以去攻克难点,防止意外的错误!以下是对多线程应用时面临的挑战介绍–以及一些如何有效解决它们的提示。
资源竞争:当每个线程都去访问同一段内存时,会导致所谓的资源竞争问题。当有多个并发线程访问共享数据时,首先访问内存数据的线程会改变共享数据–而且并不能保证哪个线程会首先访问到内存数据。你也许会假设有一个局部变量拥有你的线程最后一次写到共享内存的值,但是另一个线程也许会同时改变了共享内存的数据,然后你的局部变量就过时了!如果你知道这种情况会存在你的代码中(例如你会从多个线程同时读/写数据),就应该使用互斥锁。互斥代表互相排斥。你可以通过使用“@synchronizedblock”将实例变量包围起来,创建一个互斥锁。这样你就可以确保在互斥锁中的代码一次只能被一个线程访问:
@synchronized (self) {
myClass.object = value;
}
在以上代码中“Self”被称为一个“信号量”。当一个线程要范围这段代码时,它会检查其他的线程是否也在访问“self”。如果没有线程在访问“self”,这块代码会被执行;否则这段线程会被限制访问直到这个互斥锁解除为止。
原子性:你也许在property声明中见过很多次“nonatomic”。当你将一个property声明为atomic时,通常会把它包裹在一个@synchronized块中,确保它是线程安全的。当然,这种方法会添加一些额外的系统开销。为了更清楚的解释它,以下是一个关于atomic property的初步实现:
// If you declare a property as atomic ...
@property (atomic, retain) NSString *myString;
// ... a rough implementation that the system generates automatically,
// looks like this:
- (NSString *)myString {
@synchronized (self) {?
return [[myString retain] autorelease];
}
}
在上面的代码中,“retain”和“autorelease”被当做返回值来使用,它们被多个线程访问了,而且你不希望这个对象在多个调用之间被释放了。
所以,你先把它的值retain一下,然后把它放在自动释放池中。你可以在苹果的技术文档里面了解到更多关于 线程安全的内容。只要是大部分iOS程序员不想费心去发掘它的话,都值得去了解下。重要提示:这是一个很好的面试问题!:]
大部分的UIKit properties都不是线程安全的。想看下一个类是否是线程安全的,可以看看API文档。如果API文档没有提到任何关于线程安全的内容,你可以假设这个类是非线程安全的。
按常规,如果你正在执行一个二级的线程,而且你要对UIKit对象做操作,可以使用performSelectorOnMainThread。
死锁:一个线程被停滞,无限期地等待永远不会发生的条件。例如,如果两个线程在互相执行synchronized代码,每一个线程就会等待另一个线程完成并且打开锁。但是这种情况永远不会发生,这样两个线程都会成为死锁。
困乏时间:这会发生在有太多的线程同时执行,系统会停滞不前。NSOperationQueue有一个属性,让你设置并发线程的数量。
NSOperation API
NSOperation 类有一个相当简短的声明。要定制一个操作,可以遵循以下步骤:
1.继承NSOperation类
2.重写“main”方法
3.在“main”方法中创建一个“autoreleasepool”
4.将你的代码放在“autoreleasepool”中
创建你自己的自动释放池的原因是,你不能访问主线程的自动释放池,所以你应该自己创建一个。以下是一个例子:
#import <Foundation/Foundation.h>
@interface MyLengthyOperation: NSOperation
@end
@implementation MyLengthyOperation
- (void)main {
// a lengthy operation
@autoreleasepool {
for (int i = 0 ; i < 10000 ; i++) {
NSLog(@"%f", sqrt(i));
}
}
}
@end
上面的例子代码展示了ARC语法在自动释放池中的使用。你现在必须使用ARC了!:]
在线程操作中,你从来都不能明确知道,一个操作什么时候会开始,要持续多久才能结束。在大多数时候,如果用户滑动离开了页面,你并不想在后台执行一个操作 – 没有任何的理由让你去执行。这里关键是要经常地检查NSOperation类的isCancelled属性。例如,在上面的例子程序中,你会这样做:
@interface MyLengthyOperation: NSOperation
@end
@implementation MyLengthyOperation
- (void)main {
// a lengthy operation
@autoreleasepool {
for (int i = 0 ; i < 10000 ; i++) {
// is this operation cancelled?
if (self.isCancelled)
break;
NSLog(@"%f", sqrt(i));
}
}
}
@end
为了取消这个操作,你应该调用NSOperation的取消方法,正如下面的:
// In your controller class, you create the NSOperation
// Create the operation
MyLengthyOperation *my_lengthy_operation = [[MyLengthyOperation alloc] init];
.
.
.
// Cancel it
[my_lengthy_operation cancel];
NSOperation类有一些其他的方法和属性:
开始(start):通常,你不会重写这个方法。重写“start”方法需要相对复杂的实现,你还需要注意像isExecuting,isFinished,isConcurrent和isReady这些属性。当你将一个操作添加到一个队列当中时(一个NSOperationQueue的实例,接下来会讨论的),这个队列会在操作中调用“start”方法,然后它会做一些准备和“main”方法的后续操作。假如你在一个NSOperation实例中调用了“start”方法,如果没有把它添加到一个队列中,这个操作会在main loop中执行。
从属性(Dependency):你可以让一个操作从属于其他的操作。任何操作都可以从属于任意数量的操作。当你让操作A从属于操作B时,即使你调用了操作A的“start”方法,它会等待操作B结束后才开始执行。例如:
MyDownloadOperation *downloadOp = [[MyDownloadOperation alloc] init]; // MyDownloadOperation is a subclass of NSOperation
MyFilterOperation *filterOp = [[MyFilterOperation alloc] init]; // MyFilterOperation is a subclass of NSOperation
[filterOp addDependency:downloadOp];
要删除依赖性:
[filterOp removeDependency:downloadOp];
优先级(Priority):有时候你希望在后台运行的操作并不是很重要的,它可以以较低的优先级执行。可以通过使用“setQueuePriority:”方法设置一个操作的优先级。
[filterOp setQueuePriority:NSOperationQueuePriorityVeryLow];
其他关于设置线程优先级的选择有: NSOperationQueuePriorityLow, NSOperationQueuePriorityNormal, NSOperationQueuePriorityHigh和NSOperationQueuePriorityVeryHigh.
当你添加了操作到一个队列时,在对操作调用“start”方法之前,NSOperationQueue会浏览所有的操作。那些有较高优先级的操作会被先执行。有同等优先级的操作会按照添加到队列中的顺序去执行(先进先出)。
(历史注释:在1997年,火星车中的嵌入式系统遭遇过优先级反转问题,也许这是说明正确处理优先级和互斥锁的最昂贵示例了。
Completion block:在NSOperation 类中另一个有用的方法叫setCompletionBlock:。一旦操作完成了,如果你还有一些事情想做,你可以把它放在一个块中,并且传递给这个方法。这个块会在主线程中执行。
其他一些关于处理线程的提示:
如果你需要传递一些值和指针到一个线程中,创建你自己的指定初始化方法是一个很好的尝试:
#import Foundation/Foundation.h
@interface MyOperation : NSOperation
-(id)initWithNumber:(NSNumber *)start string:(NSString *)string;
@end
如果你的操作需要有一个返回值或者对象,声明一个委托方法是不错的选择。记住委托方法必须在主线程中返回。然而,因为你要继承NSOperation类,你必须先将这个操作类强制转换为NSObject对象。可以按照以下步骤去做:
#import<Foundation/Foundation.h>
@interfaceMyOperation:NSOperation
-(id)initWithNumber:(NSNumber*)startstring:(NSString*)string;
@end
要经常检查isCancelled属性。如果操作不需要被执行了,你就不想在后台去运行它了!
你并不需要重写“start”方法。然而,如果你决定去重写“start”方法,就必须处理好像isExecuting,isFinished,isConcurrent和isReady这些属性。否则你的操作类不会正确的运作。
你一旦添加了一个操作到一个队列(NSOperationQueue的一个实例)中,就要负责释放它(如果你不使用ARC的话)。NSOperationQueue获得操作对象的所有权,调用“start”方法,然后结束时负责释放它。
你不能重用一个操作对象。一旦它被添加到一个队列中,你就丧失了对它的所有权。如果你想再使用同一个操作类,就必须创建一个新的实例变量。
一个结束的操作不能被重启。
如果你取消了一个操作,它不会马上就发生。它会在未来的某个时候某人在“main”函数中明确地检查isCancelled==YES时被取消掉;否则,操作会一直执行到完成为止。
一个操作是否成功地完成,失败了,或者是被取消了,isFinished的值总会被设置为YES。所以千万不要觉得isFinished==YES就表示所有的事情都顺利完成了—特别的,如果你在代码里面有从属性(dependencies),就要更加注意!
NSOperationQueue API
NSOperationQueue 也有一个相当简单的界面。它甚至比NSOperation还要简单,因为你不需要去继承它,或者重写任何的方法 — 你可以简单创建一个。给你的队列起一个名字会是一个不错的做法;这样你可以在运行时识别出你的操作队列,并且让调试变得更简单:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
myQueue.name = @"Download Queue";
并发操作:队列和线程是两个不同的概念。一个队列可以有多个线程。每个队列中的操作会在所属的线程中运行。举个例子你创建一个队列,然后添加三个操作到里面。队列会发起三个单独的线程,然后让所有操作在各自的线程中并发运行。
到底有多少个线程会被创建?这是个很好的问题!这取决与硬件。默认情况下,NSOperationQueue类会在场景背后施展一些魔法,决定如何在特定的平台下运行代码是最好的,并且会尽量启用最大的线程数量。考虑以下的例子。假设系统是空闲的,并且有很多的可用资源,这样NSOperationQueue会启用比如8个同步线程。下次你运行程序,系统会忙于处理其他不相关的操作,它们消耗着资源,然后NSOperationQueue只会启用两个同步线程了。
并发操作的最大值:你可以设定NSOperationQueue可以并发运行的最大操作数。NSOperationQueue会选择去运行任何数量的并发操作,但是不会超过最大值。
myQueue.MaxConcurrentOperationCount = 3;
如果你改变了主意,想将MaxConcurrentOperationCount设置回默认值,你可以执行下列操作:
myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
添加操作:一个操作一旦被添加到一个队列中,你就应该通过传送一个release消息给操作对象(如果使用了手动引用计数,非ARC的话),然后队列会负责开始这个操作。从这点上看,什么时候调用“start”方法由这个队列说了算。
[myQueue addOperation:downloadOp];
[downloadOp release]; // manual reference counting
待处理的操作:任何时候你可以询问一个队列哪个操作在里面,并且总共有多少个操作在里面。记住只有那些等待被执行的操作,还有那些正在运行的操作,会被保留在队列中。操作一完成,就会退出队列。
NSArray *active_and_pending_operations = myQueue.operations;
NSInteger count_of_operations = myQueue.operationCount;
暂停队列:你可以通过设定setSuspended:YES来暂停一个队列。这样会暂停所有在队列中的操作 — 你不能单独的暂停操作。要重新开始队列,只要简单的setSuspended:NO。
// Suspend a queue
[myQueue setSuspended:YES];
.
.
.
// Resume a queue
[myQueue setSuspended: NO];
取消操作:要取消一个队列中的所有操作,你只要简单的调用“cancelAllOperations”方法即可。还记得之前提醒过经常检查NSOperation中的isCancelled属性吗?
原因是“cancelAllOperations”并没有做太多的工作,他只是对队列中的每一个操作调用“cancel”方法 — 这并没有起很大作用!:] 如果一个操作并没有开始,然后你对它调用“cancel”方法,操作会被取消,并从队列中移除。然而,如果一个操作已经在执行了,这就要由单独的操作去识别撤销(通过检查isCancelled属性)然后停止它所做的工作。
[myQueuecancelAllOperations];
addOperationWithBlock: 如果你有一个简单的操作不需要被继承,你可以将它当做一个块(block)传递给队列。如果你需要从块那里传递回任何数据,记得你不应该传递任何强引用的指针给块;相反,你必须使用弱引用。而且,如果你想要在块中做一些跟UI有关的事情,你必须在主线程中做。
UIImage *myImage = nil;
// Create a weak reference
__weak UIImage *myImage_weak = myImage;
// Add an operation as a block to a queue
[myQueue addOperationWithBlock: ^ {
// a block of 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
}];
}];
重新定义模型
是时候重新定义初步的非线程模型了!如果你仔细看下初步的模型,你会看到有三个线程区域可以改进。通过把这三个区域区分开来,然后把它们各自放在一个单独的线程中,主线程会获得解脱,并且可以保持对用户交互的迅速响应。
(注意:如果你不能马上理解为什么你的app运作得这么慢 — 而且有时候这并不明显 — 你应该使用Instruments工具。然而,这需要另一篇教程去讲解它了!)
为了摆脱你的程序的瓶颈限制,你需要一个特定的线程去响应用户交互事件,一个线程专门用于下载数据源和图片,还有一个线程用于执行图片滤镜处理。在新的模型中,app在主线程中开始,并且加载一个空白的table view。同时,app会开始另一个线程去下载数据源。
一旦数据源下载完毕,你会告诉table view重新加载自己。这会在主线程中完成。这个时候,table view知道有多少行,而且知道需要显示的图片的URL地址,但是它还没有实际的图片!如果你在这个时候马上开始下载所有的图片,这会非常没有效率,因为你一下子不需要所有的图片!
怎样可以把它弄得更好?
一个更好的模型就是去下载在当前屏幕可见的row的图片。所以你的代码首先会问table view哪些row是可见的,然后才会开始下载过程。还有,图片滤镜处理会在图片下载完成后才开始。因此,代码应该等待出现有一个待滤镜处理的图片时才开始进行图片滤镜处理。
为了让app的反应变得更加灵敏,代码会在图片下载完毕后马上显示,而不会等待进行滤镜处理。一旦图片的滤镜处理完成,就会更新UI以显示滤镜处理过的图片。以下是整个处理过程的控制流示意图:
为了达到这些目标,你需要去监测图片是否正在下载,或者已经完成了下载,还是图片的滤镜处理已经完成了。你还需要去监测每个操作的状态,以及判断它是一个下载操作还是一个滤镜处理操作,这样你才能在用户滚动table view的时候去做取消,中止或者恢复操作。
现在我们开始编码!
打开之前留下的那个工程,增加一个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.申明一个私有的接口,你可以改变实例变量的属性为读写(read-write)。
2.设定properties属性。
3.经常检查isCancelled变量,确保操作即时结束。
4.苹果推荐使用@autoreleasepool块,而不是alloc和initNSAutoreleasePool变量,因为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和CoreImageframeworks。你也需要导入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参数;在任何系统资源消耗较大的函数调用前后去调用这个滤镜处理函数,是不错的做法。一旦滤镜处理结束了,PhotoRecord实例的值会被恰当的设置好,然后主线程的delegate被通知了。
很好!现在你已经有了在后台线程中执行操作(operations)的所有工具和基础了。是时候回到viewcontroller然后恰当的修改它,以便它可以利用好这些新优势。
注意:在动手之前,你要下载AFNetworkinglibraryfromGitHub.
AFNetworking库是建立在NSOperation和NSOperatinQueue之上的。它提供给你很多便捷的方法,以便你不需要为普通的任务,比如在后台下载一个文件,创建你自己的操作。
当需要从互联网下载一个文件的时候,在适当的位置写一些代码来检查错误是个不错的做法。下载数据源,一个只有4kBytes的propertylist,不是什么大问题,你并不需要操心去为它创建一个子类。然而,你不能假设会有一个可靠持续的网络连接。
苹果为此提供了NSURLConnection类。使用它会是一项额外的工作,特别是当你只是想下载一个小的propertylist时。AFNetworking是一个开源代码库,提供了一种非常方便的方式去实施这类任务。你要传入两个块(blocks),一个在操作成功时传入,另一个在操作失败时传入。接下来你会看到相关的实践例子。
要添加这个库到工程中,选择File>AddFilesTo…,然后浏览选择你下载好的AFNetworking文件夹,最后点击"Add"。确保选中了"Copyitemsintodestinationgroup'sfolder"选项!是的,你正在使用ARC,但是AFNetworking还没有从陈旧的手动管理内存的泥潭中爬出来。
如果你遵循着安装指南,就可以避免编译错误,如果你不遵循的话,你会在编译时去处理非常多的错误。每一个AFNetworking模块需要在你的Target'sBuildPhases标签包含"-fno-objc-arc"字段,它在CompilerFlags部分下面。
要实现它,在导航栏(在左手边)点击"PhotoRecords"。在右手边,选择"Targets"下面的"ClassicPhotos"。从标签栏选择"BuildPhases"。在它下面,选择三角形展开"CompileSources"项。选上属于AFNetworking的所有文件。敲击Enter键,一个对话框就会弹出来。在对话框中,输入"fno-objc-arc",然后点击"Done"。
切换到ListViewController.h文件,然后根据以下内容更新头文件:
切换到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.你可以从ListViewController头文件中删除CoreImage,因为你不再需要它了。然而,你需要导入PhotoRecord.h文件,PendingOperations.h,ImageDownloader.h和ImageFiltration.h文件。
2.这里是对AFNetworking库的引用。
3.确保让ListViewController遵从ImageDownloader和ImageFiltration的delegate方法。
4.你不再需要这样的数据源。你将要使用propertylist来创建PhotoRecord的实例。所以,将"photos"类从NSDictionary修改为NSMutableArray,这样你就可以更新图片数组了。
5.这个property被用来监测等待操作(operations)。
切换到ListViewController.m文件,然后根据以下内容进行更新:
切换到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.在下载数据时,通过启动网络活动指示器(networkactivityindicator)来提供用户反馈。
4.通过使用setCompletionBlockWithSuccess:failure:,你可以添加两个块(blocks):一个给操作成功的情况,另一个给操作失败的情况。
5.在成功的块中,以NSData的数据格式下载propertylist,然后通过使用toll-freebriding桥,将参数强制转换成CFDataRef和CFPropertyList,再将propertylist文件转换成NSDictionary。
6.创建一个NSMutableArray,然后在字典中循环申明所有的objects和key,创建一个PhotoRecord实例,然后保存它到数组中。
7.一旦完成了,将_photo对象指向records数组,重新加载tableview然后停止网络活动指示器。你还要释放"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的accessoryview。
2.数据源包含PhotoRecord的所有实例。根据行(row)的indexPath参数,从photos数组中获取并创建相应的PhotoRecord实例。
3.检查PhotoRecord。看它的图片是否已经下载完了,显示了图片,图片的名字,然后停止了活动指示器(activityindicator)。
4.如果下载图片失败,显示一个预留图片来提示失败情况,然后停止活动指示器(activityindicator)。
5.否则,图片还没有被下载下来。开始下载和图片滤镜处理操作(它们现在还没有被实现),然后显示一个预留图片表示你正在对它进行处理。启动活动指示器(activityindicator)来提醒用户有操作正在进行。
现在是时候来实现负责启动操作的方法了。如果你还没有实现它,可以在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吗?你可以看到一点小小的努力就可以让你的应用程序的响应变得更加灵敏 - 并且让用户觉得更加有趣!
进一步调整
你已经在本篇教程中进展很久了!你的小工程比起原来的版本变得更加反应灵敏,有了很大的提升。然而,仍然有一些细节需要去处理。你想成为一个优秀的程序员,而不仅仅是好的程序员!
你也许已经注意到当你在tableview中滚动时,那些屏幕以外的cell仍然处于下载和滤镜处理的进程中。难道你没有在代码里面设置取消操作?是的,你有-你应该好好的利用它们!:]
回到Xcode,切换到ListViewController.m文件中。来到tableView:cellForRowAtIndexPath:的方法实现,如下所示,将[selfstartOperationsForPhotoRecord:aRecordatIndexPath: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、一旦用户开始滑动了,你就应将所有的操作挂起。后面将会实现suspendAllOperations方法。
2、假如decelerate的值是NO,那就意味着用户停止拖动这个tableview了。因此你想要恢复挂起的操作,取消那些屏幕外面的cell的操作,开始屏幕内的cell的操作。后面将会实现loadImagesForOnscreenCells和resumeAllOperations的方法。
3、这个delegate方法是告诉你tableview停止滚动了,所做的和第二步一样的做法。
现在就来实现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];
}
编译运行工程,你会看到一个响应更加灵敏,有更好的资源管理的应用程序!
现在还可以做什么?
这是工程改进后的完整代码。
如果你完成了这个工程,并且花时间真正理解了它,恭喜!相比刚阅读本教程时,你可以把自己看待成一个更有价值的iOS开发者了!大部分的开发工作室都会幸运的拥有一两个能真正理解这些原理的人。
但是注意-像deeply-nestedblocks(块),无理由地使用线程会让维护你的代码的人难以理解。线程会引来不易察觉的bugs,只有当网络缓慢时才会出现,或者当代码运行在一个更快(或者更慢)的设备中,或者有不同内核数目的设备中。仔细认真的测试,经常使用Instruments(或者是你自己的观察)来核实引入的线程是否真的取得了性能提升。
http://www.cocoachina.com/applenews/devnews/2012/1010/4899.html
http://www.baidu.com/link?url=bF0LQqGa-li7v-SyFIN7ck-_nagzsAhmsyxQ370OCxaUa1-aNjzL0PWaPP5ztCu4nPFU811zIPHUhbzfDR8VIVwexbQZdO2uUCvqqV7JAna