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,实现起来会方便很多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容