iOS多线程之NSOperationQueue

说到iOS多线程,大部分人应该立马就想到了GCD(Grand Central Dispatch) ,因为GCD使用起来方便,代码逻辑也清晰。但是,GCD也不是万能的,有些功能,GCD实现起来比较复杂,而NSOperation Queue就会比较简单。本文围绕NSOperation Queue相比于GCD的一些优势,说说NSOperation Queue的使用。

注:本文中使用操作队列代替NSOperationQueue,使用操作任务代替NSOperation

有了GCD,为什么还有要用NSOperationQueue

  • GCD是底层的C语言构成的API,而NSOperationQueue以及相关对象是基于GCD的Objective-C对象的封装,作为一个对象,NSOperationQueue为我们提供了更多的选择
  • NSOperationQueue任务可以很方便的取消(也只能取消未执行的任务),而GCD没法停止已经加入队列的任务(其实是有的,但需要许多复杂的代码)
  • 不像GCD那样的是按FIFO顺序来执行的,NSOperation能够方便地通过依赖关系设置操作执行顺序,可以控制任务在特定的任务执行完后才执行;而GCD要实现这个功能的话,就需要通过barrier或者group来控制执行顺便,如果依赖关系复杂的话,代码逻辑就非常复杂了
  • NSOperation支持KVO(Key-Value Observing),可以方便的监听任务的状态(完成、执行中、取消等等状态)
  • NSOperation可以设置同一个队列中任务的优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码
  • 还可以通过自定义NSOperation,封装任务逻辑,提高整个代码的复用度

NSOperation

NSOperation相当于是NSOperationQueue中一个操作任务。NSOperation本身是一个抽象类,不能直接使用。我们可以使用系统提供的子类NSInvocationOperation 和NSBlockOperation,或者自己实现NSOperation子类的方式来执行操作任务。NSOperation对象都是通过调用start方法来开始执行任务。

NSInvocationOperation

该NSOperation子类可以通过设置@selector的方法来执行任务。是同步方法,调用start方法开始执行任务

self.invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocation:) object:@"invocation object"];
[self.invocationOperation start];

- (void)invocation:(NSString *)params {
    NSLog(@"handler invocationOperation with params:%@",params);
    NSLog(@"invocationOperation thread:%@",[NSThread currentThread]);
}

控制台输出

2016-09-24 11:34:55.713 handler invocationOperation with params:invocation object
2016-09-24 11:34:55.713 invocationOperation thread:<NSThread: 0x7fc602d06600>{number = 1, name = main}
2016-09-24 11:34:55.713 after invocationOperation

从控制台输出可以看出,该操作并不是新启一个线程执行,而是在当前线程上同步执行的。

NSBlockOperation

该NSOperation子类可以在其block中执行相关线程操作,也是同步方法。可以通过addExecutionBlock方法添加并发执行任务,但是其并发操作只是在该NSBlockOperation对象的内部,NSBlockOperation对象必须在其所有线程执行完毕才算执行完成,会同步等待所有执行的线程。可以参照如下代码:

// 创建NSBlockOperation对象
self.blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"blockOperation1 thread:%@",[NSThread currentThread]);
}];
[self.blockOperation addExecutionBlock:^{
    NSLog(@"blockOperation2 thread:%@",[NSThread currentThread]);
}];
[self.blockOperation addExecutionBlock:^{
    NSLog(@"blockOperation3 thread:%@",[NSThread currentThread]);
}];

[self.blockOperation start];
NSLog(@"after blockOperation");

控制台输出

2016-09-24 11:47:03.956 blockOperation1 thread:<NSThread: 0x7fd97af02950>{number = 1, name = main}
2016-09-24 11:47:03.957 blockOperation3 thread:<NSThread: 0x7fd97af11160>{number = 3, name = (null)}
2016-09-24 11:47:03.957 blockOperation2 thread:<NSThread: 0x7fd97af00e00>{number = 2, name = (null)}
2016-09-24 11:47:03.958 after blockOperation

从打印结果可以看出,第一个blockOperation1是在当前线程(主线程)中执行的,其他操作都是另启线程执行,而NSBlockOperation只有其内部是并发执行的,其本身还是同步执行的

completionBlock

NSOperation操作都有一个completionBlock属性,可以用监听操作执行完毕

[self.invocationOperation setCompletionBlock:^{
    NSLog(@"invocationOperation completion");
}];

[self.blockOperation setCompletionBlock:^{
    NSLog(@"blockOperation completion");
}];

NSOperationQueue

如果不是使用操作任务的start来方法来启动操作任务,可以通过添加操作对象到操作队列的方式来执行任务,操作队列默认会将添加的操作任务启一个线程来执行来并发执行。也可以通过addOperationWithBlock:block方法直接在操作队列中添加任务执行。

添加到操作队列中的操作不需要调用start方法,系统会自动调度执行操作队列中的任务。

[self.queue addOperation:self.blockOperation];
[self.queue addOperation:self.invocationOperation];
[self.queue addOperationWithBlock:^{
    NSLog(@"queueOperationBlock thread:%@",[NSThread currentThread]);
}];
NSLog(@"after queue");

控制台输出

2016-09-24 12:05:30.484 after queue
2016-09-24 12:05:30.485 blockOperation2 thread:<NSThread: 0x7fb392d756f0>{number = 3, name = (null)}
2016-09-24 12:05:30.484 handler invocationOperation with params:invocation object
2016-09-24 12:05:30.485 blockOperation3 thread:<NSThread: 0x7fb392d759b0>{number = 4, name = (null)}
2016-09-24 12:05:30.485 blockOperation1 thread:<NSThread: 0x7fb392f04330>{number = 2, name = (null)}
2016-09-24 12:05:30.486 invocationOperation thread:<NSThread: 0x7fb392e77030>{number = 5, name = (null)}
2016-09-24 12:05:30.486 queueOperationBlock thread:<NSThread: 0x7fb392d759b0>{number = 4, name = (null)}
2016-09-24 12:05:30.487 blockOperation thread:<NSThread: 0x7fb392d759b0>{number = 4, name = (null)}

从打印结果可以看出,操作队列中的操作都是新启线程并发执行的

添加依赖

操作队列中操作任务对象可以通过添加依赖关系来控制执行顺序,使用addDependency:来添加依赖,removeDependency:来删除依赖关系。以下代码表示blockOperation 依赖于invocationOperation,即blockOperation要等待invocationOperation执行完毕后才能执行,对之前代码添加依赖。

[self.blockOperation addDependency:self.invocationOperation];

控制台输出

2016-09-24 12:15:50.284 queueOperationBlock thread:<NSThread: 0x7fa479f0a020>{number = 2, name = (null)}
2016-09-24 12:15:50.284 after queue
2016-09-24 12:15:50.284 handler invocationOperation with params:invocation object
2016-09-24 12:15:50.285 invocationOperation thread:<NSThread: 0x7fa479e2a050>{number = 3, name = (null)}
2016-09-24 12:15:50.286 blockOperation2 thread:<NSThread: 0x7fa479e2a050>{number = 3, name = (null)}
2016-09-24 12:15:50.286 blockOperation1 thread:<NSThread: 0x7fa479f031c0>{number = 4, name = (null)}
2016-09-24 12:15:50.286 blockOperation3 thread:<NSThread: 0x7fa479f0a020>{number = 2, name = (null)}
2016-09-24 12:15:50.287 blockOperation thread:<NSThread: 0x7fa479f031c0>{number = 4, name = (null)}

从打印结果可以看出,blockOperation操作是在invocationOperation操作执行完后才开始执行

注:依赖关系必须在添加到操作队列之前设置才有效果;添加依赖后,操作要等待所有依赖操作执行完毕(操作被取消也算完成)后,才能开始执行;注意操作任务间不要出现循环依赖,会导致死锁。

对于不在操作队中的操作任务,如果依赖的操作未完成或者并未开始执行,这个时候调用start方法,则会造成崩溃。如以下情形

[self.blockOperation addDependency:self.invocationOperation];
[self.blockOperation start];

以上代码,blockOperation依赖于invocationOperation,而invocationOperation并没有调用start,即没有开始执行任务,此时调用[self.blockOperation start];会造成崩溃

取消操作

对于单个操作任务对象,可以调用cancel取消有未执行的操作

操作队列,可以调用cancelAllOperations取消队列中所有未执行的操作(已经在执行的操作还是不能取消)。

[self.blockOperation cancel];
[self.invocationOperation cancel];
[self.queue cancelAllOperations];

注:取消操作对于在操作队列中的任务,只是将还没执行的操作任务将其从操作队列中移除,并且更新其依赖关系,对于正在执行的操作任务,将不起作用。对于未在操作队列中的单个操作任务,取消操作只是将其标记为取消状态,具体操作还取决于其start或者main方法中对于取消状态的处理

设置优先级

操作任务有以下优先级选项

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
[self.blockOperation setQueuePriority:NSOperationQueuePriorityHigh];
[self.invocationOperation setQueuePriority:NSOperationQueuePriorityLow];

注:设置优先级只针对同一队列中的操作,而且在必须start或者加入队列之前设置才有效果。操作的执行优先级还取决于其依赖关系和添加队列的顺序,如果其依赖的操作未执行完毕或者和其相同优先级的操作在其之前添加到队列,那该操作的执行顺序要延后

暂停和继续

可以通过以下方法暂停和继续操作队列

[self.queue setSuspended:YES]; //暂停
[self.queue setSuspended:NO];  //继续

注:暂停一个操作队列不会导致正在执行的操作任务中途暂停,只是简单地阻止调度新操作任务执行

设置最大并发数

可以通过maxConcurrentOperationCount属性设置最大并发数,该值默认为-1,由系统调度。当设置为1的时候相当于是一个同步执行的操作队列。

等待 NSOperation 执行完成

单个操作任务对象可以调用waitUntilFinished来阻塞当前线程等待操作完成

[self.invocationOperation waitUntilFinished];

操作队列可以调用waitUntilAlloperationsAreFinished等待操作队列中的所有操作执行完毕。

[self.queue waitUntilAllOperationsAreFinished];

注:当我们在等待一个 操作队列 中的所有操作任务 执行完成时,其他的线程仍然可以向这个 操作队列中添加 操作任务 ,从而延长我们的等待时间。

监听属性

操作任务中有一些属性可以供开发者监听,来处理各种状态

  • isExecuting 代表任务正在执行中
  • isFinished 代表任务已经执行完成,被取消也算执行完成
    注:该状态关系到依赖其的操作任务,只有在其isFinished状态为YES的时候,依赖其的操作任务才能开始执行,操作队列也是根据这个状态来决定是否将操作任务从队列中移除
  • isCancelled 代表任务已经取消执行
  • isAsynchronous 代表任务是并发还是同步执行,
    注:当操作任务加入到操作队列后,会忽略该属性
  • isReady 代表任务是否已经准备执行
    注:当其依赖的操作任务都执行完时,改状态才会是YES

自定义NSOperation

当开发者希望封装复杂的操作时,可以自定义NSOperation。如何创建一个自定义的NSOperation,取决于这个NSOperation是被设计为同步还是异步

自定义同步NSOperation

只需要重写main()方法

- (void)main {
    NSLog(@"Operation main");
    //operation implementation
}

注:重写的main方法,不会自动更新isFinished状态。所以如果使用自定义的操作,重写mian中没有手动更新isFinished状态,则如果有操作任务依赖该自定义操作,是在操作队列中是永远无法执行的,或者会崩溃(不在操作队列的情况)

自定义并发NSOperation

需要重写start()isAsynchronousisExecutingisFinished

  • isAsynchronous需要返回YES,代表操作队列是并发的
  • isExecuting该状态应该维护,确保其他可以被调用者正确监听操作状态,应该确保该操作是线程安全的
  • isFinished该状态确保操作完成或者取消的时候,都被正确的更新,不然,如果操作不是完成状态,则操作队列不会把改操作从队列中移除,不然会导致依赖其的操作任务无法执行,该操作应该也确保该操作是线程安全的
  • isExecuting和isFinished都必须通过KVO的方法来通知状态更新

在start方法开始的时候,应该监听isCancelled状态,如果已经被取消直接结束操作,避免对于的开销

//DemoOperation.m
@interface DemoOperation ()
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@end

@implementation DemoOperation
@synthesize finished = _finished, executing = _executing;

- (void)start {
    @synchronized (self) {
        self.executing = YES;
        NSLog(@"DemoOperation start");
        if (self.isCancelled) {
            self.executing = NO;
            self.finished = YES;
        }
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            self.executing = NO;
            self.finished = YES;
        });
    }
}

- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isAsynchronous {
    return YES;
}

注:当实现了start方法时,默认会执行start方法,而不执行main方法

总结

虽然在iOS开发中,多线程方法大部分使用的还是GCD,但是对于某些特殊需求,如取消任务、设置任务执行顺序、任务状态监听、复杂任务封装等还是推荐使用NSOperationQueue,实现起来会方便很多。

推荐阅读更多精彩内容