Cocoa operations以面向对象的形式封装程序中的工作单元,提供异步执行机制。Operations可以独立运行,也可以与Operation Queue协同运行。OS X和IOS中Cocoa应用程序大多运用了operation机制。Operations可以提高程序的并发处理能力,可以将应用程序中的某些行为或处理逻辑封装进独立的工作单元,减少应用主线程的的压力,提供流畅的用户界面。
Operation对象是NSOperation类的实例,用以封装执行程序中独立的工作单元。NSOperation类为抽象类,定义并提供了子类所需的关键方法和基础架构,应用程序需要根据实际需求,继承并实现NSOperation类的子类,来处理自己的程序工作单元。为了减少应用程序的类的数量和复杂性,Foundation框架提供了两个NSOperation的具体子类以供使用:
1.NSInvocationOperation
创建NSInvocationOperation对象,提供用以调用的指定对象的以及该对象特定方法的selector。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@implementation
AppCustomClass
- (NSOperation*)createInvocationOperation:(id)params
{
NSInvocationOperation* operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:
@selector
(customTaskMethod:)
object:params];
return
operation;
}
// 实际的程序的工作单元
- (
void
)customTaskMethod:(id)params
{
// to do the task.
}
@end
|
2.NSBlockOperation
与NSInvocationOperation一样同属NSOperation的具体子类,但不同的是以block的形式对程序进行封装,但block本身没有提供显示的传入参数和返回结果。
1
2
3
|
NSBlockOperation* blockOperation = [NSBlockOperation blockOperationWithBlock: ^{
// to do the task.
}];
|
在创建blockOperation后可以调用addExecutionBlock:方法添加更多的block
自定义Operation对象
NSInvocationOperation和NSBlockOperation并不会满足应用程序的所有需求,可以通过直接继承NSOperation来添加所需的处理逻辑。之前提到过,NSOperation提供了子类通用的模板方法,来处理operation间依赖关系和保证KVO兼容性。实现非并发型的operation只要实现main task并正确响应取消事件即可。并发型的operation则稍微复杂点,可能需要替换NSOperation已有的方法。
实现自定义NSOperation的最小操作包括:
- 提供一个自定义的初始化方法,在执行operation之前提供初始化流程,准备operation的状态。
- 覆盖main方法,实现自己的处理逻辑。
此外,自定义的NSOperation子类可能还需要添加额外的工作,包括
- 提供在main方法中需要调用的自定义方法
- 提供数据对象和operation返回值的存取器方法
- 实现NSCoding协议,添加对operation对象的序列化支持。
自定义NSOperation子类的简单的代码模板如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// MoveFileOperation.h
@interface
MoveFileOperation : NSOperation
- (id)initWithSrcURL:(NSURL *)srcURL toDestinationURL:(NSURL*)desURL;
@end
// MoveFileOperation.m
@interface
MoveFileOperation ()
@property
(retain) NSURL *srcURL;
@property
(retain) NSURL *desURL;
@end
@implementation
MoveFileOperation
@synthesize
rootURL, queue;
- (id)initWithSrcURL:(NSURL *)srcURL toDestinationURL:(NSURL*)desURL;
{
self = [
super
init];
if
(self) {
self.srcURL = srcURL;
self.desURL = desURL;
}
return
self;
}
- (
void
)main
{
if
([self isCancelled]){
return
;
}
NSDirectoryEnumerator *itr =
[[NSFileManager defaultManager] enumeratorAtURL:self.srcURL
includingPropertiesForKeys:nil
options:(NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants)
errorHandler:nil];
NSError *error = [NSError
new
];
for
(NSURL *url in itr) {
if
([self isCancelled]) {
break
;
}
else
{
NSString *fileName = [url lastPathComponent];
NSURL *desFileURL = [self.desURL URLByAppendingPathComponent:fileName];
[[NSFileManager defaultManager] copyItemAtURL:url toURL:desFileURL error:&error];
}
}
}
|
正确的响应Cancel事件
加入到操作队列的operation对象会一直运行下去,直至任务完成或程序代码显示对其进行取消操作。取消operation的行为可能发生在任意时刻,
即使是在operation开始执行之前。NSOperation类提供了取消operation的方法,但对于取消行为的处理需要程序自己去控制,如果operation被直接取消,那么可能就无法正确的释放之前分配的一些资源,所以通常情况下,运行中的operation在取消时,程序需要以正确的方式释放资源并退出。
只要在operation运行中定期的在合适的位置调用isCancelled方法,并在当返回值为YES时直接return即可。可以此来支持operation的取消行为。支持operation对象的取消行为是很重要的,不管operation对象所属类是NSOperation的具体子类还是自定义类。isCancelled方法本身是轻量级的,调用本身不会带来任何性能影响。在程序逻辑中,常见的正确调用isCancelled的位置包括:
- 在实际执行工作之前
- 在每个循环的执行体中,如果一次执行体耗时很长可酌情添加调用次数
- 程序相对容易终止的代码处。
简单的代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
- (
void
)main
{
BOOL isDone = NO;
while
(![self isCancelled] && !isDone) {
....
if
([self isCancelled]){
return
;
}
...
}
}
|
维护NSOperation的KVO兼容性
NSOperation维护以下key的KVO兼容性
- isCancelled
- isConcurrent
- isExecuting
- isFinished
- isReady
- dependencies
- queuePriority
- completionBlock
覆盖了start方法的子类需要维护isConcurrent,isExecuting的KVO兼容性
没有覆盖 addDependency: 或 removeDependency:方法的子类,不用关系dependencies的KVO兼容性
基本上没有必要来显示的生成其它属性的KVO通知。
配置operation的依赖关系
依赖关系保证不同的operation以串行化的方式顺序执行。依赖于其它operation执行结果的operation对象,只有等待它们都完成之后才能执行。所以可以以这种方式来创建复杂的operation对象依赖图。
调用addDependency: 方法可以建立两个operation对象间的依赖关系。该方法创建了目标对象到当前对象之间的单向依赖关系。这个单向关系表明当前operation对象只有在目标operation对象执行完成之后才能执行。operation对象的依赖关系不限于同一个操作队列。有依赖关系的operation对象可以添加到不同的操作队列中。但是operation之间不能添加循环依赖关系。
operation的依赖关系建立在operation对象间KVO消息的发送。自定义的operation对象需要在自定义代码中的合适位置来添加KVO通知。
添加Completion Block
在OS X v10.6及以后,operation可以在任务完成后执行一个completion block。可以在这个block中执行main task之外的工作。例如,可以在此处派发operation完成的消息。并发的operation可以使用该block来生成最终的KVO通知。
通过setCompletionBlock: 方法可以完成添加completion block的工作。
Operation对象的内存管理
-
避免单独的线程存储(Per-Thread Storage)
多数的operation对象都运行在由操作队列提供的同一个线程中,操作队列被认为是该线程的所有者,并且该线程对于operation对象应该是透明的。不要将数据关联到那些非自己创建和持有的线程对象上。操作队列管理的线程的创建和释放依赖于系统和应用的需求。因此,通过在同一个线程对象上存储被所有operation共用的共享数据(Per-Thread storage),通常会失败。
此外,其它情况下也不要使用Per-Thread storage。初始化operation对象时,需要提供其完成任务所需的所有数据。因此,应该
是operation对象本身提供了执行环境上下文所需的数据。所有的数据都应该存储在operation对象上,直到应用程序本身使用完毕为止。 -
按需保存指向operation对象的引用
因为operation对象是以异步的方式运行,所以你不能在创建完operation对象之后就对它置之不理。它们和普通对象一样需要你来管理指向它的引用,以此来支持其它代码的运行。尤其是当你需要从执行完毕的operation对象上获取数据。维护operation对象引用的原因很简单,因为在operation对象添加到操作队列之后,你不能在重新获得operation对象。操作队列 会以最快的速度的派发和执行operation。许多情况下是,operation对象在添加到操作队列之后会马上运行。当你的代码需要向操作 队列获取之前添加的operation对象时,该对象可能已运行完毕并从队列中移除,亦即,执行完毕的operation对象会马上被释放掉。
-
处理错误和异常
以独立实体存在于应用程序中的operation对象需要自己处理任何错误和异常。 OS X v10.6及以后的版本中,NSOperation类提供 的start方法不会捕获异常。你的程序代码需要捕获和抑制异常,并按需通知应用程序的的其它部分。
如果程序确实遇到了异常和错误,你需要采取任何措施,保证错误不会传播到应用程序中。NSOperation类没有提供显示的方法来处理 错误结果代码或异常,并提供给应用程序。因此,如果这些信息对你的应用程序来说很重要,你需要自己提供所需的代码。
决定Operation对象的合适的适用范围
虽然可以向操作队列中添加任意数量的operation对象,但这么做其实是不现实的。如其它对象一样,NSoperation类的对象实例也是要消耗内存,并执行也会耗时。创建成千上万的完成小功能的operation对象,因过度派发和执行,会导致超出任务本身所需消耗的不良结果。如果应用程序本身的内存用量已很紧俏,那么在内存中包含了成千上万的operation对象会进一步降低你应用的性能。
使用operation对象的关键在于,在任务工作量与保证计算机时刻处于繁忙状态之间找到一个合理的平衡点。尝试确保operation对象所完成工作的合理性。例如,同时创建100个完成同样工作的operation对象,则不如分10次,每次同时创建10个该operation对象。
避免在同一个操作队列中一次添加大量的operation对象,避免添加operation的速度超过operation本身的执行速度的情况。一股脑的将所有operation对象添加到操作队列,则不如按批次添加。当一个批次完成之后,利用completion block来告诉应用程序来创建下一批次。有大量任务要处理时,尽量塞满操作队列会保证机器重复运转,但大量的operation对象会马上耗光你得内存。
总而言之,在使用operation时,尽量在效率和时间之间找到平衡点。
执行Operation
1 .将operation对象添加到操作队列
这是执行operation对象的最简单的方法,操作队列属于NSOperatinQueue类。应用程序负责创建和维护操作队列,Application可以创建任意数量的操作队列,但实际上operation的的并行执行数量是有限制的。操作队列会和系统一起来限制operation的并发执行数量,以保持合理的系统负载。所以,创建额外的操作队列不意味着你可以执行额外的操作。
1
2
3
4
5
6
7
8
9
10
11
12
|
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
// Add a single operation
[aQueue addOperation:anOp];
// Add multiple operations
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO];
// Add a block
[aQueue addOperationWithBlock:^{
/* Do something. */
}];
|
NSOperationQueue设计上是用来并发执行operation,但可以通过强制措施来保证一次运行一个operation。setMaxConcurrentOperationCount:方法可以设置操作队列并行执行的最大数量。设置为1可以保证一次运行一个operation,但operation的执行顺序却仍依赖于其它因素,如operation的就绪状态,优先级设置。所以串行化操作队列与串行化的GCD转发队列不同。需要设置operation间的依赖关系来保证它们的执行顺序是你想要的。
2 .手动执行Operation
可以在没有操作队列的前提下手动执行operation,但需要一些前提条件。尤其是operation对象必须准备好运行,并保证是调用start方法来执行。
一个operation只有在其isReady方法返回YES时才被认为是可运行的。isReady方法会被整合进NSOperation的依赖管理系统来保证operation的依赖状态。只有在依赖关系清楚后,operation才开始运行。
在手动执行operation时,必须调用其start方法。start方法在正式执行你得代码之前会做几个安全性检测。默认的start方法会生成operation依赖关系所需的KVO通知。同时保证已取消的operation不会再执行,以及在operation没就绪就开始运行时抛出异常。
取消Operation
一旦加入操作队列,操作队列就拥有了该operation,并且它不能被删除。调出operation的唯一方法是取消(cancel)它。在一个operation上调用cancel可以取消其执行,在操作队列对象上调用cancelAllOperations 会取消所有operation。
只有在你确知你不需要operation对象时,才可以把它们取消掉。operation的取消行为也会被认为是执行完毕(finished),依赖于它的其它operation对象会收到KVO通知来清除该依赖关系。因此,常见的行为是响应特殊的事件来取消队列中所有的operation,比如程序退出或用户显示进行取消。
等待Operations结束
为了获得最佳性能,程序应该尽量设计为异步的,以便在operations执行时application可以自由处理其它工作。如果创建operation的进程需要处理operation的结果,可以使用NSOperation的 waitUntilFinished方法来阻塞代码直到operation完成。但通常很少这样去做。阻塞当前线程可能是一种便捷的解决方法,但并没有向你的代码添加更多的序列化行为或限制并发量。
更重要的是,决不能在主线程上等待一个operation结束。应该创建其它线程来执行该操作。阻塞主线程会导致应用程序无法正常的接受用户事件,用户界面会暂时性的失去响应,这是在界面开发时应极力避免的,这种交互体验是极差的。
在NSOperationQueue上调用waitUntilAllOperationsAreFinished 可以等待队列中所有的线程执行完之后在调用指定的回调。
挂起和回复队列
调用NSOperationQueue对象的setSuspended: 方法可以操作队列挂起。挂起队列不会阻碍当前正在执行中的operation,它只是阻止新的operation的执行。为了响应用户请求,你可以关起任何当前正在处理的工作,因为用户可能会最终重新恢复队列执行。
原文:http://my.oschina.net/hmj/blog/135629