简介
NSOperation的作用:实现多线程编程
具体步骤
- 先将需要执行的操作封装到一个NSOperation对象中
- 然后将NSOperation对象添加到NSOperationQueue中
- 系统会自动将NSOperationQueue中的NSOperation取出来
- 将取出的NSOperation封装的操作放到一条新线程中执行
NSOperation的子类
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
使用NSOperation子类的方式有3种
- NSInvocationOperation
- NSBlockOperation
- 自定义子类继承NSOperation,实现内部相应的方法
NSInvocationOperation和NSBlockOperation
- NSInvocationOperation
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//1. 创建任务,封装任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(task1) object:nil];
//2. 启动/执行
[op1 start];
[self task1];
}
-(void)task1{
NSLog(@"%s----%@",__func__,[NSThread currentThread]);
}
打印结果
2021-04-10 11:39:56.589533+0800 NSOperation01-基本使用[4783:154245] -[ViewController task1]----<NSThread: 0x600002a886c0>{number = 1, name = main}
2021-04-10 11:39:56.589671+0800 NSOperation01-基本使用[4783:154245] -[ViewController task1]----<NSThread: 0x600002a886c0>{number = 1, name = main}
可以看到封装任务启动任务和直接调用task1
方法是一样的结果,且是在主线程中执行而没有开启子线程,因为NSOperation需要配合NSOperationQueue才能开启多线程,而上面示例没有使用队列
- blockOperation
-(void)blockOperation{
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2----%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3----%@",[NSThread currentThread]);
}];
[op1 start];
[op2 start];
[op3 start];
}
打印结果
2021-04-10 11:49:53.951734+0800 NSOperation01-基本使用[4960:163855] 1----<NSThread: 0x600003b381c0>{number = 1, name = main}
2021-04-10 11:49:53.951886+0800 NSOperation01-基本使用[4960:163855] 2----<NSThread: 0x600003b381c0>{number = 1, name = main}
2021-04-10 11:49:53.952009+0800 NSOperation01-基本使用[4960:163855] 3----<NSThread: 0x600003b381c0>{number = 1, name = main}
可以看到任务在主线程中串行执行,也没有开启多线程
但是我们可以通过NSBlockOperation
的addExecutionBlock
方法在操作中追加任务,如下
-(void)blockOperation{
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1----%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2----%@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3----%@",[NSThread currentThread]);
}];
[op1 addExecutionBlock:^{
NSLog(@"4----%@",[NSThread currentThread]);
}];
[op2 addExecutionBlock:^{
NSLog(@"5----%@",[NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"6----%@",[NSThread currentThread]);
}];
[op1 start];
[op2 start];
[op3 start];
}
2021-04-10 11:50:21.339177+0800 NSOperation01-基本使用[4992:164915] 1----<NSThread: 0x6000014e80c0>{number = 1, name = main}
2021-04-10 11:50:21.339184+0800 NSOperation01-基本使用[4992:165002] 4----<NSThread: 0x6000014ad0c0>{number = 5, name = (null)}
2021-04-10 11:50:21.339374+0800 NSOperation01-基本使用[4992:164915] 2----<NSThread: 0x6000014e80c0>{number = 1, name = main}
2021-04-10 11:50:21.339387+0800 NSOperation01-基本使用[4992:165002] 5----<NSThread: 0x6000014ad0c0>{number = 5, name = (null)}
2021-04-10 11:50:21.339614+0800 NSOperation01-基本使用[4992:164915] 3----<NSThread: 0x6000014e80c0>{number = 1, name = main}
2021-04-10 11:50:21.339653+0800 NSOperation01-基本使用[4992:165002] 6----<NSThread: 0x6000014ad0c0>{number = 5, name = (null)}
现在有三个操作、六个任务,每个操作有对应的2个任务,可以看到在执行任务1后开启子线程执行任务4(任务4是追加在操作1中的),实现了多线程,同一个操作中的任务数量大于1时,任务在操作中的执行是并发执行的,不保证顺序,且追加的任务不一定是在子线程中执行,也可能是主线程
结合NSOperationQueue使用
NSOperationQueue有两种类型
- 主队列
- 和GCD中的主队列一样
[NSOperationQueue mainQueue];
- 非主队列
- 非常特殊:同时具备并发和串行的功能(默认情况下是并发)
[[NSOperationQueue alloc]init];
代码示例
- NSInvocationOperation
-(void)invocationOperation{
//1. 创建任务,封装任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(task1) object:nil];
//2. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//3. 添加操作到队列中
[queue addOperation:op1]; // 在这个方法内部自动调用[op1 start];
}
打印结果
2021-04-10 15:28:15.018135+0800 NSOperation02-NSOperationQueue的基本使用[7569:271371] -[ViewController task1]----<NSThread: 0x60000233c240>{number = 3, name = (null)}
可以看到开启了子线程
- NSBlockOperation
-(void)blockOperationWithQueue{
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%s----%@",__func__,[NSThread currentThread]);
}];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:op1];
}
打印结果
2021-04-10 15:32:16.835307+0800 NSOperation02-NSOperationQueue的基本使用[7640:275382] -[ViewController blockOperationWithQueue]_block_invoke----<NSThread: 0x600000741f80>{number = 5, name = (null)}
也是实现了开启子线程,如果有多个操作,是并发执行的
还有一种简便方法向队列中添加任务,其内部操作其实也是先创建操作,然后将操作添加到队列中,如下
//简便方法
[queue addOperationWithBlock:^{
NSLog(@"%s----%@",__func__,[NSThread currentThread]);
}];
自定义NSOperation
自定义的好处
- 有利于代码隐蔽
- 有利于代码复用
使用方法
- 先自定义一个子类ZSOperation继承自NSOperation
- 正常的操作
// ViewController.m
-(void)customWithQueue{
//1. 封装操作
ZSOperation *op1 = [[ZSOperation alloc]init];
ZSOperation *op2 = [[ZSOperation alloc]init];
//2. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//3. 添加操作到队列
[queue addOperation:op1];
[queue addOperation:op2];
}
- 在子类中重写main方法
// ZSOperation.m
//告知要执行的任务是什么
-(void)main{
NSLog(@"main----%@",[NSThread currentThread]);
}
NSOperation的其他方法
设置最大并发数
刚刚上面说到非主队列的默认情况是并发的,那么我们如何设置成串行的呢?这就需要用到最大并发数maxConcurrentOperationCount
maxConcurrentOperationCount
的默认值是-1,-1在计算机的含义是最大值
代码示例
-(void)test{
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//设置最大并发数
queue.maxConcurrentOperationCount = 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]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"4-----%@",[NSThread currentThread]);
}];
NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"5-----%@",[NSThread currentThread]);
}];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
[queue addOperation:op5];
}
打印结果
2021-04-10 16:27:33.474160+0800 NSOperation03-其他用法[8550:317629] 2-----<NSThread: 0x600003340e00>{number = 6, name = (null)}
2021-04-10 16:27:33.474167+0800 NSOperation03-其他用法[8550:317630] 1-----<NSThread: 0x60000335b480>{number = 5, name = (null)}
2021-04-10 16:27:33.474397+0800 NSOperation03-其他用法[8550:317629] 3-----<NSThread: 0x600003340e00>{number = 6, name = (null)}
2021-04-10 16:27:33.474444+0800 NSOperation03-其他用法[8550:317630] 4-----<NSThread: 0x60000335b480>{number = 5, name = (null)}
2021-04-10 16:27:33.474552+0800 NSOperation03-其他用法[8550:317629] 5-----<NSThread: 0x600003340e00>{number = 6, name = (null)}
最大并发数控制的不是线程数量,而是一个队列中最多可以有多少个操作同时参与并发执行
当希望是串行任务的时候,就将maxConcurrentOperationCount=1
即可,可以看到下面结果只开启了一条线程,且任务串行
打印结果
2021-04-10 16:30:44.333695+0800 NSOperation03-其他用法[8613:320761] 1-----<NSThread: 0x600003de1e80>{number = 7, name = (null)}
2021-04-10 16:30:44.333940+0800 NSOperation03-其他用法[8613:320761] 2-----<NSThread: 0x600003de1e80>{number = 7, name = (null)}
2021-04-10 16:30:44.334065+0800 NSOperation03-其他用法[8613:320761] 3-----<NSThread: 0x600003de1e80>{number = 7, name = (null)}
2021-04-10 16:30:44.334181+0800 NSOperation03-其他用法[8613:320761] 4-----<NSThread: 0x600003de1e80>{number = 7, name = (null)}
2021-04-10 16:30:44.334308+0800 NSOperation03-其他用法[8613:320761] 5-----<NSThread: 0x600003de1e80>{number = 7, name = (null)}
暂停、继续、取消
队列中的操作也是有状态的
- 等待
- 执行
- 结束 三种事件
- 暂停:
[self.queue setSuspended:YES];
- 是可以恢复的
- 点击暂停并不会立刻暂停,而是等待正在执行的操作执行完毕后才暂停
- 继续:
[self.queue setSuspended:NO];
- 取消:
[self.queue cancelAllOperations];
- 不可恢复
- 点击取消并不会立刻取消,而是等待正在执行的操作执行完毕后才取消
- 内部实现了队列中所有操作的cancel
上面说的三种事件在系统自带的两种操作类型是那样的,然而,在自定义的操作中,情况会有所不同
// ViewController.m
self.queue = [[NSOperationQueue alloc]init];
self.queue.maxConcurrentOperationCount = 1;
ZSOperation *op1 = [[ZSOperation alloc]init];
[self.queue addOperation:op1];
// ZSOperation.m
- (void)main{
// 3个耗时操作
for (NSInteger i = 0; i < 10000; i++) {
NSLog(@"download1 --- %zd --- %@",i,[NSThread currentThread]);
}
NSLog(@"+++++++++++++++++++++++++++++++++++++++");
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"download2 --- %zd --- %@",i,[NSThread currentThread]);
}
NSLog(@"+++++++++++++++++++++++++++++++++++++++");
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"download3 --- %zd --- %@",i,[NSThread currentThread]);
}
}
这种情况下点击暂停是没有用的:因为如果是自定义的操作类,我们也只定义了一个操作添加到队列中,所以还是需要等待操作执行完毕才可以暂停
- 如果希望可以实现取消,可以在重写的main方法中加一个判断
if (self.isCancelled) return;
,完整代码如下
- (void)main{
// 3个耗时操作
for (NSInteger i = 0; i < 10000; i++) {
NSLog(@"download1 --- %zd --- %@",i,[NSThread currentThread]);
}
if (self.isCancelled) return;
NSLog(@"+++++++++++++++++++++++++++++++++++++++");
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"download2 --- %zd --- %@",i,[NSThread currentThread]);
}
if (self.isCancelled) return;
NSLog(@"+++++++++++++++++++++++++++++++++++++++");
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"download3 --- %zd --- %@",i,[NSThread currentThread]);
}
if (self.isCancelled) return;
}
这样在正在执行download1这个耗时操作时点击取消,在这个for循环完毕后就会return了,操作也就取消了
- 如果希望实现在点击取消时立刻停止,可以将判断
if (self.isCancelled) return;
放在耗时操作内部即可,代码如下
for (NSInteger i = 0; i < 10000; i++) {
if (self.isCancelled) return;
NSLog(@"download1 --- %zd --- %@",i,[NSThread currentThread]);
}
但是不推荐在耗时操作中加这个判断,会影响性能(假设循环要十万次,那么也要进行十万次判断....)
NSOperation操作依赖和操作监听
操作依赖
其实可以理解为控制操作的顺序,通过addOperation
来实现
-(void)dependency{
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
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]);
}];
//添加操作依赖
[op1 addDependency:op2];
[op3 addDependency:op4];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
}
2021-04-10 21:07:43.007735+0800 NSOperation04-操作依赖和监听[11737:466406] 4---<NSThread: 0x600001ebf080>{number = 6, name = (null)}
2021-04-10 21:07:43.007762+0800 NSOperation04-操作依赖和监听[11737:466411] 2---<NSThread: 0x600001ec0300>{number = 5, name = (null)}
2021-04-10 21:07:43.007905+0800 NSOperation04-操作依赖和监听[11737:466411] 1---<NSThread: 0x600001ec0300>{number = 5, name = (null)}
2021-04-10 21:07:43.007949+0800 NSOperation04-操作依赖和监听[11737:466408] 3---<NSThread: 0x600001ec5c40>{number = 4, name = (null)}
可以看到操作1是在操作2完成后才执行,操作3是在操作4完成后才执行
也可以设置跨队列依赖,假设再创建第二个队列,将操作4添加到该队列中,再设置依赖也是可以的
操作监听
例如我们的需求是希望再某些操作执行完毕后会有通知,实现是通过completionBlock
-(void)dependency{
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
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]);
}];
//添加监听
op3.completionBlock = ^{
NSLog(@"操作执行完毕啦!");
};
//添加操作依赖
[op1 addDependency:op2];
[op3 addDependency:op4];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
}
打印结果
2021-04-10 21:16:35.196159+0800 NSOperation04-操作依赖和监听[11906:477095] 4---<NSThread: 0x600000f8b680>{number = 7, name = (null)}
2021-04-10 21:16:35.196198+0800 NSOperation04-操作依赖和监听[11906:477092] 2---<NSThread: 0x600000f88700>{number = 5, name = (null)}
2021-04-10 21:16:35.196442+0800 NSOperation04-操作依赖和监听[11906:477095] 3---<NSThread: 0x600000f8b680>{number = 7, name = (null)}
2021-04-10 21:16:35.196568+0800 NSOperation04-操作依赖和监听[11906:477092] 1---<NSThread: 0x600000f88700>{number = 5, name = (null)}
2021-04-10 21:16:35.196645+0800 NSOperation04-操作依赖和监听[11906:477095] 操作执行完毕啦!--<NSThread: 0x600000f8b680>{number = 7, name = (null)}
可以看到在执行完操作3后会打印(但并不一定是立即打印,因为是并发执行的),且并不一定会在同一条线程
线程的通信
还是通过追加操作的方式实现addOperationWithBlock
,不过要注意操作的队列应该是主队列,因为更新UI的操作要在主线程中完成
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
NSBlockOperation *download = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn.sinaimg.cn%2Fsinacn%2F20170612%2Faeee-fyfzsyc2496417.jpg&refer=http%3A%2F%2Fn.sinaimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1620654759&t=95a4ba561b2a946b12992943bef34a71"];
NSData *imageData = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"download--------%@",[NSThread currentThread]);
//更新UI,需要在主线程中操作
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
NSLog(@"更新UI--------%@",[NSThread currentThread]);
}];
}];
[queue addOperation:download];
}
打印结果
2021-04-10 21:57:47.864482+0800 NSOperation05-线程间的通信[12567:509199] download--------<NSThread: 0x600002357780>{number = 5, name = (null)}
2021-04-10 21:57:47.865169+0800 NSOperation05-线程间的通信[12567:508929] 更新UI--------<NSThread: 0x600002340240>{number = 1, name = main}
再举一个例子,合并图片
-(void)combie{
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
__block UIImage *image1;
__block UIImage *image2;
NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
//下载图片1
NSURL *url = [NSURL URLWithString:@"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn.sinaimg.cn%2Fsinacn%2F20170612%2Faeee-fyfzsyc2496417.jpg&refer=http%3A%2F%2Fn.sinaimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1620654759&t=95a4ba561b2a946b12992943bef34a71"];
NSData *imageData = [NSData dataWithContentsOfURL:url];
image1 = [UIImage imageWithData:imageData];
NSLog(@"download1--------%@",[NSThread currentThread]);
}];
NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
//下载图片2
NSURL *url = [NSURL URLWithString:@"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fn.sinaimg.cn%2Fsinacn%2F20170612%2Faeee-fyfzsyc2496417.jpg&refer=http%3A%2F%2Fn.sinaimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1620654759&t=95a4ba561b2a946b12992943bef34a71"];
NSData *imageData = [NSData dataWithContentsOfURL:url];
image2 = [UIImage imageWithData:imageData];
NSLog(@"download2--------%@",[NSThread currentThread]);
}];
NSBlockOperation *combie = [NSBlockOperation blockOperationWithBlock:^{
//1. 开启上下文
UIGraphicsBeginImageContext(CGSizeMake(800, 800));
//2. 画图1
[image1 drawInRect:CGRectMake(0, 0, 800, 400)];
//3. 画图2
[image2 drawInRect:CGRectMake(0, 400, 800, 400)];
//4. 根据上下文得到图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
//5. 关闭上下文
UIGraphicsEndImageContext();
//更新UI,需要在主线程中操作
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
NSLog(@"更新UI--------%@",[NSThread currentThread]);
}];
}];
//设置依赖
[combie addDependency:download1];
[combie addDependency:download2];
[queue addOperation:download1];
[queue addOperation:download2];
[queue addOperation:combie];
}