iOS多线程-NSOperation

495 阅读10分钟

简介

NSOperation的作用:实现多线程编程

具体步骤

  1. 先将需要执行的操作封装到一个NSOperation对象中
  2. 然后将NSOperation对象添加到NSOperationQueue中
  3. 系统会自动将NSOperationQueue中的NSOperation取出来
  4. 将取出的NSOperation封装的操作放到一条新线程中执行

NSOperation的子类

NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类

使用NSOperation子类的方式有3种

  • NSInvocationOperation
  • NSBlockOperation
  • 自定义子类继承NSOperation,实现内部相应的方法

NSInvocationOperation和NSBlockOperation

  1. 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才能开启多线程,而上面示例没有使用队列

  1. 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}

可以看到任务在主线程中串行执行,也没有开启多线程

但是我们可以通过NSBlockOperationaddExecutionBlock方法在操作中追加任务,如下

-(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]; 代码示例
  1. 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)}

可以看到开启了子线程

  1. 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

自定义的好处

  • 有利于代码隐蔽
  • 有利于代码复用

使用方法

  1. 先自定义一个子类ZSOperation继承自NSOperation
  2. 正常的操作
//  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];
}
  1. 在子类中重写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}

image.png 再举一个例子,合并图片

-(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];
}

Simulator Screen Shot - iPhone 11 - 2021-04-10 at 22.18.10.png