NSOpertation 与NSOperationQueue
GCD 虽然好用,但是GCD 的介面让我们不太容易取消已经排程、或是已经在背景执行的作业,如果我们有中途取消某个背景作业的需要,使用NSOperation 与NSOperation Queue 会是更好的选择。
NSOperation 是一个用来封装一项作业的Objective-C class,这样的物件称为operation,在建立了一个operation 后,便可以将operation 丢入opeation queue(型别为NSOperationQueue)中排程,让opeation queue决定在适当的时机、系统可以负荷的状况下,执行我们排入排程的工作。
无论是NSOperationQueue 与NSOperation,都有与取消工作相关的API 可以调用。我们也可以设定每个opeation 的优先程度,以及不同operation 之间的相依关系(dependency),要求一件工作完成之后,才可以继续下一件工作。
建立NSOperationQueue
假如我们有一个Class,里头有一个operation queue,只要调用alloc
与init
便可以建立。
#import <Foundation/Foundation.h>
@interface Test : NSObject
@property (nonatomic, strong) NSOperationQueue *queue;
@end
@implementation Test
- (instancetype)init
{
self = [super init];
if (self) {
self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 2;
}
return self;
}
@end
我们可以透过maxConcurrentOperationCount
这个property,设定NSOperationQueue可以同时平行执行几件工作,如果超过1,就代表允许平行执行,如果刚好是1的话,就代表在这个queue当中的所有工作都会依次执行。预设值是-1(NSOperationQueueDefaultMaxConcurrentOperationCount),意思是让系统自己决定最多可以同时建立多少thread。
我们可以对NSOperationQueue呼叫addOperation:
加入operation,用cancelAllOperations
取消所有排程中的作业。至于已经在执行中的作业,我们就得对特定的operation调用cancel
了。
建立NSOperation
在Cocoa/Cocoa Touch Framework 中,已经存在两个NSOperation 的subclass: NSBlockOperation 与NSInvocationOperation。NSBlockOperation可以让你把一个block 封装成NSOperation,至于NSInvocationOperation 则是用来封装NSInvocation。
一般来说,除非是像前面说的,你希望这些工作在排程中就可以取消,或是要特别指定operation 之间的相依关系,不然,要在背景执行某个block 或是invocation,其实使用GCD API 会更容易。
我们可能会更常建立自己的NSOperation subclass,处理更复杂的背景工作。
比方说,我们现在要开发一套食谱App,这套App 可以让用户在本机的编辑介面中编好一份食谱后上传,上传后要清除本机的暂存档,这份食谱可能会包含一份包含标题、内文的JSON 档案,还有一张图片,所以上传食谱这份工作就包含上传JSON 文件与图片两件工作,而我们也希望可以在上传的过程中随时取消,让用户继续编辑再重新上传—这种比较复杂却又带有次序性质的工作,就是很适合NSOperation 的舞台。
要subclass 一个NSOperation,最重要的就是要override 掉main 这个method,main 这个method 里头代表的是这个operation 要做什么事情。我们现在可以来写我们的operation:
@interface RecipetUploadOperation : NSOperation
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *JSON;
@end
@implementation RecipetUploadOperation
- (void)main
{
@autoreleasepool {
// 1. Upload image
// 2. Upload JSON
}
}
@end
在main 里头,我们也要建立auto release pool。
接下来我们会遇到一个问题:在上传照片与JSON 档案的时候,我们会呼叫NSURLSession 的相关API,这些API 都是非同步的,但是在main 这个method 里头,如果不做特别的处理,还没等到连线回应,main 就已经执行结束了。我们必须要想办法停在main 中,等待连线API 的回应。
在Operation 中等待与取消
要在operation 的中途停下来等候回应,我们大致上有两种作法,一种是在operation 当中执行NSRunloop,另外一种则是使用GCD 当中的semaphore。
NSRunloop
在有GCD 之前,我们希望一个operation 可以在一个地方停下来等候其他事情发生,作法会是在这条thread 里头执行run loop。
前一章提到,run loop就是那个「之所以GUI程式会一直执行,而不会像某个function或method从头到尾跑完就结束」的回圈。在iOS或Mac OS X App中,除了在main thread会执行最主要的run loop ([NSRunloop mainRunLoop]
)之外,每个thread/operation里头,也会有属于各自的run loop,只要调用[NSRunloop currentRunLoop]
,调用的就是属于当前thread自己的run loop—所以我们要注意,虽然在不同的thread中,我们调用的都是[NSRunloop currentRunLoop]
,但这个+currentRunLoop
这个class method回传的并不视同一个实例。另外, NSRunloop不可以手动建立,我们只能使用系统提供的run loop物件。
我们希望能够在这个operation执行到一半的时候可以被取消,要取消一条operation,便是调用NSOperation的cancel
这个method,因为我们subclass了NSOperation,改变了operation里头做的事情,那么也就得override掉cancel
:当我们的operation在跑run loop时,我们的cancel
必须要能够通知run loop停止。
当一条thread在跑自己的run loop之后,如果不同thread之间想要互相沟通,那我们就必须在当前的thread建立NSPort物件,并且将NSPort物件注册到run loop内,才能让讯息传递到run loop里头。所以,当外部要求对port调用invalidate
的时候,就会让run loop收到讯息,停止继续跑,继续执行-main
这个method接下来的动作。
NSPort也有对应的Core Foundation实作,像CFMessagePort等,不过在iOS 7之后我们没办法在这个地方使用CFMessagePort。从iOS 7之后,调用CFMessagePortCreateLocal
或CFMessagePortCreateRemote
这些建立CFMessagePort的function都无法建立实例,只会回传NULL(可以参见CFMessagePort的reference),苹果不允许我们使用CFMessagePort的原因是,CFMessagePort不但可以传递讯息到其他thread的run loop上,甚至可以传到其他process的run loop上,而iOS政策上禁止process互相沟通。
在iOS 7 刚问世的时候,苹果又完全没有说清楚这件事,只忙着宣传iOS 7 的扁平化新设计。我们为了CFMessagePort 的这项改变,还在WWDC 2013 会场上跑了两天的Lab。
范例程式如下:
@interface RecipetUploadOperation : NSOperation
{
NSPort *port;
BOOL runloopRunning;
}
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *JSON;
@end
@implementation RecipetUploadOperation
- (void)main
{
@autoreleasepool {
[someAPI uploadImageData:UIImagePNGRepresentation(self.image) callback:^ {
[self quitRunLoop];
}];
[self doRunloop];
if (self.isCancelled) {
return;
}
[someAPI uploadJSON:self.JSON callback:^ {
[self quitRunLoop];
}];
[self doRunloop];
}
}
- (void)doRunloop
{
runloopRunning = YES;
port = [[NSPort alloc] init];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSRunLoopCommonModes];
while (runloopRunning && !self.isCancelled) {
@autoreleasepool {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
}
}
port = nil;
}
- (void)quitRunLoop
{
[port invalidate];
runloopRunning = NO;
}
- (void)cancel
{
[super cancel];
[self quitRunLoop];
}
@end
GCD Semaphores
有了GCD 之后,很多事情都变得简单许多。当我们想要在执行到一半的时候暂停下来,现在可以选择建立semaphore,接着:
- 只要对semaphore调用
dispatch_semaphore_wait
,程式就会在这个地方暂停等候。 - 对已经在等候中的semaphore,再调用
dispatch_semaphore_signal
,发送signal,程式就会继续往下运行。
范例程式如下:
@import UIKit;
@interface RecipetUploadOperation : NSOperation
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *JSON;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@end
@implementation RecipetUploadOperation
- (void)main
{
@autoreleasepool {
self.semaphore = dispatch_semaphore_create(0);
[someAPI uploadImageData:UIImagePNGRepresentation(self.image) callback:^ {
dispatch_semaphore_signal(self.semaphore);
}];
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
if (self.cancelled) {
return;
}
self.semaphore = dispatch_semaphore_create(0);
[someAPI uploadJSON:self.JSON callback:^ {
dispatch_semaphore_signal(self.semaphore);
}];
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
}
}
- (void)cancel
{
[super cancel];
dispatch_semaphore_signal(self.semaphore);
}
@end