iOS中NSOperation详解
分享是每个优秀的程序员所必备的品质
内容提要:
- 基本概念
- 3种子类的使用以及和队列queue的配合使用
- 设置最大并发数
- 队列的暂停和恢复以及取消
- 添加操作依赖
- 操作的监听
- 场景中的综合使用
概念
- NSOperation是对GCD的包装
- 两个核心概念:queue(队列)、 Operation(操作)
- NSOperation和NSOperationQueue结合使用实现多线程并发
NSOperation的子类
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类,使用NSOperation子类的方式有3种
- NSInvocationOperation
- NSBlockOperation
- 自定义子类继承NSOperation,实现内部相应的方法
NSInvocationOperation
直接上代码:
-(void) invocationOpeation {
//1.创建操作,封装任务
/*
第三个参数object:前面方法需要接受的参数 可以为nil
*/
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation1) object:nil];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation2) object:nil];
NSInvocationOperation *op3 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation3) object:nil];
//2.启动|执行操作
[op1 start];
[op2 start];
[op3 start];
}
-(void)operation1{
NSLog(@"1--%@",[NSThread currentThread]);
}
-(void)operation2{
NSLog(@"2--%@",[NSThread currentThread]);
}
-(void)operation3{
NSLog(@"3--%@",[NSThread currentThread]);
}
打印:
RCNSOperationDemo[1368:53571] 1--<NSThread: 0x6000039cb5c0>{number = 1, name = main}
RCNSOperationDemo[1368:53571] 2--<NSThread: 0x6000039cb5c0>{number = 1, name = main}
RCNSOperationDemo[1368:53571] 3--<NSThread: 0x6000039cb5c0>{number = 1, name = main}
是不是觉得没什么软用,还不如不用!NSInvocationOperation只有配合NSOperationQueue使用才能实现多线程编程(后面会说到),单独使用NSInvocationOperation不会开启线程,默认在当前线程(指执行该方法的线程)中同步执行。
NSBlockOperation
和NSInvocationOperation相似,只是将方法替换成了Block,并可以使用addExecutionBlock追加任务
直接上代码:
-(void)blockOperation{
//1.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2----%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3----%@",[NSThread currentThread]);
}];
//追加任务
//注意:如果一个操作中的任务数量大于1,那么会开子线程并发执行任务
//注意:不一定是子线程,有可能是主线程
[op3 addExecutionBlock:^{
NSLog(@"4---%@",[NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"5---%@",[NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"6---%@",[NSThread currentThread]);
}];
//2.启动
[op1 start];
[op2 start];
[op3 start];
}
打印:
// 第一次打印:
RCNSOperationDemo[1479:60441] 1----<NSThread: 0x600000b0a940>{number = 1, name = main}
RCNSOperationDemo[1479:60441] 2----<NSThread: 0x600000b0a940>{number = 1, name = main}
RCNSOperationDemo[1479:60441] 4---<NSThread: 0x600000b0a940>{number = 1, name = main}
RCNSOperationDemo[1479:60491] 3----<NSThread: 0x600000b5f800>{number = 3, name = (null)}
RCNSOperationDemo[1479:60544] 5---<NSThread: 0x600000b5f780>{number = 4, name = (null)}
RCNSOperationDemo[1479:60543] 6---<NSThread: 0x600000b56480>{number = 5, name = (null)}
// 第n次打印:
RCNSOperationDemo[1479:60441] 1----<NSThread: 0x600000b0a940>{number = 1, name = main}
RCNSOperationDemo[1479:60441] 2----<NSThread: 0x600000b0a940>{number = 1, name = main}
RCNSOperationDemo[1479:60491] 3----<NSThread: 0x600000b5f800>{number = 3, name = (null)}
RCNSOperationDemo[1479:60969] 6---<NSThread: 0x600000b54440>{number = 9, name = (null)}
RCNSOperationDemo[1479:60968] 4---<NSThread: 0x600000b40d80>{number = 8, name = (null)}
RCNSOperationDemo[1479:60441] 5---<NSThread: 0x600000b0a940>{number = 1, name = main}
可以得出单独使用NSBlockOperation和NSInvocationOperation一样,默认在当前线程中同步执行。
但是使用addExecutionBlock追加的任务是并发执行的,如果这个操作中的任务数量大于1,那么会开子线程并发执行任务,并且追加的任务不一定就是子线程,也有可能是主线程。所以上述中任务1、2、3执行是可期的,有序的,但是任务4、5、6是并发执行的,不可控的。
自定义继承自NSOperation的类
自定义一个继承自NSOperation的类RCOperation,在.m中重写main方法即可,main方法中就是要执行的操作,代码如下:
#import "RCOperation.h"
@implementation RCOperation
// 重写main方法,适用于代码量较多,功能较复杂的操作
-(void)main{
// 执行的任务
NSLog(@"main---%@",[NSThread currentThread]);
}
@end
调用:
- (void)customOpeation {
//1.封装操作
RCOperation *op1 = [[RCOperation alloc]init];
RCOperation *op2 = [[RCOperation alloc]init];
//2.启动
[op1 start];
[op2 start];
}
就是这么简单,下面来点不简单的
NSOperationQueue
NSOperation中的两种队列
- 主队列 通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行,这点很重要!!!记着。
- 非主队列 直接alloc init出来的队列。非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行
NSInvocationOperation和NSOperationQueue组合:
上代码:
- (void)invocationOperationWithQueue {
//1.创建操作,封装任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation1) object:nil];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation2) object:nil];
NSInvocationOperation *op3 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation3) object:nil];
//2.创建队列
/*
GCD:
串行类型:create & 主队列
并发类型:create & 全局并发队列
NSOperation:
主队列: [NSOperationQueue mainQueue] 和GCD中的主队列一样,串行队列
非主队列: [[NSOperationQueue alloc]init] 非常特殊(同时具备并发和串行的功能)
//默认情况下,非主队列是并发队列
*/
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//3.添加操作到队列中,addOperation方法内部已经调用了[op1 start],不需要再手动启动
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
}
打印:
RCNSOperationDemo[2036:91039] 3--<NSThread: 0x600002d97580>{number = 4, name = (null)}
RCNSOperationDemo[2036:91040] 2--<NSThread: 0x600002d97680>{number = 5, name = (null)}
RCNSOperationDemo[2036:91036] 1--<NSThread: 0x600002d97500>{number = 3, name = (null)}
创建的队列中的任务默认是异步执行的
NSBlockOperation和NSOperationQueue组合:
上代码:
- (void)blockOperationWithQueue {
//1.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2----%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3----%@",[NSThread currentThread]);
}];
//追加任务
[op2 addExecutionBlock:^{
NSLog(@"4----%@",[NSThread currentThread]);
}];
[op2 addExecutionBlock:^{
NSLog(@"5----%@",[NSThread currentThread]);
}];
[op2 addExecutionBlock:^{
NSLog(@"6----%@",[NSThread currentThread]);
}];
//2.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//3.添加操作到队列
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
//提供一个简便方法,使用Block直接添加任务
//1)创建操作,2)添加操作到队列中
[queue addOperationWithBlock:^{
NSLog(@"7----%@",[NSThread currentThread]);
}];
}
打印:
RCNSOperationDemo[2101:94606] 1----<NSThread: 0x6000002b7340>{number = 3, name = (null)}
RCNSOperationDemo[2101:94724] 3----<NSThread: 0x6000002ba5c0>{number = 4, name = (null)}
RCNSOperationDemo[2101:94725] 7----<NSThread: 0x6000002ba680>{number = 5, name = (null)}
RCNSOperationDemo[2101:94605] 2----<NSThread: 0x6000002b75c0>{number = 6, name = (null)}
RCNSOperationDemo[2101:94726] 4----<NSThread: 0x6000002ba700>{number = 7, name = (null)}
RCNSOperationDemo[2101:94606] 6----<NSThread: 0x6000002b7340>{number = 3, name = (null)}
RCNSOperationDemo[2101:94727] 5----<NSThread: 0x6000002b7640>{number = 8, name = (null)}
任务都是并发执行的
自定义的Operation和NSOperationQueue组合:
- (void)customWithQueue{
//1.封装操作
RCOperation *op1 = [[RCOperation alloc]init];
RCOperation *op2 = [[RCOperation alloc]init];
//2.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//3.添加操作到队列
[queue addOperation:op1];
[queue addOperation:op2];
}
NSOperation其它用法
设置最大并发数【控制任务并发和串行】
最大并发数是队列在同一时间中最多有多少个任务可以执行
static const NSInteger NSOperationQueueDefaultMaxConcurrentOperationCount = -1;
默认是-1
-(void)maxConcurrentTest {
//1.创建队列
//默认是并发队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//2.设置最大并发数量 maxConcurrentOperationCount
/*
同一时间最多有多少个任务可以执行
串行执行任务!=只开一条线程 (线程同步)
maxConcurrentOperationCount >1 那么就是并发队列
maxConcurrentOperationCount == 1 那就是串行队列
maxConcurrentOperationCount == 0 不会执行任务
maxConcurrentOperationCount == -1 特殊意义 最大值 表示不受限制
*/
queue.maxConcurrentOperationCount = 1;
//3.封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2----%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3----%@",[NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"4----%@",[NSThread currentThread]);
}];
//4.添加到队列
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
}
设置最大并发送=1打印:
RCNSOperationDemo[2295:105303] 1----<NSThread: 0x600003496940>{number = 3, name = (null)}
RCNSOperationDemo[2295:105302] 2----<NSThread: 0x6000034addc0>{number = 4, name = (null)}
RCNSOperationDemo[2295:105303] 3----<NSThread: 0x600003496940>{number = 3, name = (null)}
RCNSOperationDemo[2295:105303] 4----<NSThread: 0x600003496940>{number = 3, name = (null)}
可见,设置最大并发送=1,队列是按顺序串行执行任务的。
注意,串行执行任务 != 只开一条线程 ,可以开启多条线程,只不过是以线程同步的方式执行的,就像加了互斥锁,区别队列里的任务是串行执行的还是并发执行的,不是看它开了多少条线程,而是看任务的执行方式,是有序的还是无序的。
队列的暂停和恢复以及取消
- 暂停和恢复队列
// YES代表暂停队列,NO代表恢复队列
- (void)setSuspended:(BOOL)b;
注意:暂停操作不能使当前正在处于执行状态的任务暂停,而是该任务执行结束,后面的任务不会执行,处于排队等待状态 。例如执行2个任务,在执行第1个任务时,执行了暂停操作,第1个任务不会立即暂停,而是第1个任务执行结束后,所有任务暂停,即第2个任务不会再执行。
- 取消队列的所有操作
// 跟暂停相似,当前正在执行的任务不会立即取消,而是后面的所有任务永远不再执行,且该操作是不可以恢复的
- (void)cancelAllOperations;
提示:也可以调用NSOperation的 cancel 方法取消单个操作
以自定义的NSOperation为例,执行一个任务,任务包含3个耗时操作(1个任务中包含3个操作)。
需求:执行取消操作的时候,后面的耗时操作不会再执行(3个耗时操作还是属于同一个任务,区别1个queue包含3个operation)
代码:
RCOperation.h中
#import "RCOperation.h"
@implementation RCOperation
// 重写main方法,适用于代码量较多,功能较复杂的操作
-(void)main{
//耗时操作1
for (int i = 0; i<1000; i++) {
// 一般不会讲判断放在耗时操作里面,判断多次,耗费性能
// if(self.isCancelled) return;
NSLog(@"任务1-%d--%@",i,[NSThread currentThread]);
}
//苹果官方建议,每当执行完一次耗时操作之后,就查看一下当前队列是否为取消状态,如果是,那么就直接退出,以此提高程序的性能
if(self.isCancelled) return;
NSLog(@"+++++++++++++++++++++++++++++++++");
//耗时操作2
for (int i = 0; i<1000; i++) {
NSLog(@"任务2-%d--%@",i,[NSThread currentThread]);
}
if(self.isCancelled) return;
NSLog(@"+++++++++++++++++++++++++++++++++");
//耗时操作3
for (int i = 0; i<1000; i++) {
NSLog(@"任务3-%d--%@",i,[NSThread currentThread]);
}
}
@end
执行各种操作代码:
// 开始
- (IBAction)startBtnClick:(id)sender{
//1.创建队列
//默认是并发队列
self.queue = [[NSOperationQueue alloc]init];
//2.设置最大并发数量 maxConcurrentOperationCount
self.queue.maxConcurrentOperationCount = 1;
RCOperation *op = [[RCOperation alloc]init];
//4.添加到队列
[self.queue addOperation:op];
}
// 暂停
- (IBAction)suspendBtnClick:(id)sender{
//设置暂停和恢复
//suspended设置为YES表示暂停,suspended设置为NO表示恢复
//暂停表示不继续执行队列中的下一个任务,暂停操作是可以恢复的
/*
队列中的任务也是有状态的:已经执行完毕的 | 正在执行 | 排队等待状态
*/
//不能暂停当前正在处于执行状态的任务
[self.queue setSuspended:YES];
}
// 继续
- (IBAction)goOnBtnClick:(id)sender{
//继续执行
[self.queue setSuspended:NO];
}
// 取消
- (IBAction)cancelBtnClick:(id)sender{
//取消队列里面的所有操作
//取消之后,当前正在执行的操作的下一个操作将不再执行,而且永远不再执行,就像后面的所有任务都从队列里面移除了一样
//取消操作是不可以恢复的
//该方法内部调用了所有操作的cancel方法
[self.queue cancelAllOperations];
}
苹果官方建议,每当执行完一次耗时操作之后,就查看一下当前队列是否为取消状态,如果是,那么就直接退出,以此提高程序的性能 。
操作依赖
NSOperation之间可以设置依赖来保证执行顺序,比如:操作A执行完后,才能执行操作B,就可以使用操作依赖
[opB addDependency: opA]; // 操作B依赖于操作A
而且可以在不同queue的NSOperation之间创建依赖关系,比如:操作A在队列1中,操作B在队列2中,也可以使用addDependency
还保证执行顺序。
注意:不可以循环依赖:
// 不可以循环依赖
[opB addDependency: opA];
[opA addDependency: opB];
循环依赖的结果:循环依赖的操作都不会有任何的执行,不会发生异常,并且不会影响该队列的其他操作
操作的监听
可以监听一个操作的执行完毕
- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;
操作依赖和操作的监听使用代码:
- (void)dependencyTest{
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
NSOperationQueue *queue2 = [[NSOperationQueue alloc]init];
//2.封装操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1---%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2---%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3---%@",[NSThread currentThread]);
}];
//操作监听
op3.completionBlock = ^{
NSLog(@"3已经执行完了------%@",[NSThread currentThread]);
};
//添加操作依赖
[op1 addDependency:op3]; //跨队列依赖,op1属于queue,op3属于queue2
[op2 addDependency:op1];
//添加操作到队列
[queue addOperation:op1];
[queue addOperation:op2];
[queue2 addOperation:op3];
}
打印:
RCNSOperationDemo[3204:155297] 3---<NSThread: 0x60000276c400>{number = 3, name = (null)}
RCNSOperationDemo[3204:155297] 1---<NSThread: 0x60000276c400>{number = 3, name = (null)}
RCNSOperationDemo[3204:155370] 3已经执行完了------<NSThread: 0x60000276c900>{number = 4, name = (null)}
RCNSOperationDemo[3204:155297] 2---<NSThread: 0x60000276c400>{number = 3, name = (null)}
由依赖可知优先级:op3 > op1 > op2,
注意:监听的操作不一定和被监听的操作同一个线程,都是异步的,只是op3执行结束,肯定会执行监听的操作。
NSOperation实现线程间通信
设置操作依赖来实现线程间通信
使用场景:在子线程下载两张图片,下载完毕后绘制在UIImageView中
代码:
- (void)downloadImage{
__block UIImage *image1;
__block UIImage *image2;
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//2.封装操作下载图片1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1907928680,2774802011&fm=26&gp=0.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
//拿到图片数据
image1 = [UIImage imageWithData:data];
}];
//3.封装操作下载图片2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1412439743,1735171648&fm=26&gp=0.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
//拿到图片数据
image2 = [UIImage imageWithData:data];
}];
//4.合成图片
NSBlockOperation *drawOp = [NSBlockOperation blockOperationWithBlock:^{
//4.1 开启图形上下文
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
//4.2 画image1
[image1 drawInRect:CGRectMake(0, 0, 200, 100)];
//4.3 画image2
[image2 drawInRect:CGRectMake(0, 100, 200, 100)];
//4.4 根据图形上下文拿到图片数据
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// NSLog(@"%@",image);
//4.5 关闭图形上下文
UIGraphicsEndImageContext();
//7.回到主线程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
self.imageView.image = image;
NSLog(@"刷新UI---%@",[NSThread currentThread]);
}];
}];
//5.设置操作依赖
[drawOp addDependency:op1];
[drawOp addDependency:op2];
//6.添加操作到队列中执行
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:drawOp];
}
开启两个异步的子线程来下载图片,添加操作依赖,使2张图片下载结束后,绘制图片。回到主线程显示图片。
RCNSOperationDemo
以上基本上满足NSOperation在日常开发中使用了!
因为最近有小伙伴找我询问关于iOS多线程方面的知识,我怕误人子弟,有遗漏的地方就把以前的东西整理一下,希望可以帮助更多的人!