iOS中NSOperation详解

作者: HuyaRC | 来源:发表于2019-03-07 14:35 被阅读2次

    分享是每个优秀的程序员所必备的品质


    内容提要:

    • 基本概念
    • 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多线程方面的知识,我怕误人子弟,有遗漏的地方就把以前的东西整理一下,希望可以帮助更多的人!

    个人浅见,有误的地方欢迎指正

    相关文章

      网友评论

        本文标题:iOS中NSOperation详解

        本文链接:https://www.haomeiwen.com/subject/kkgqpqtx.html