多线程之NSOperation的简单使用

来源:互联网 发布:网络管理需求分析 编辑:程序博客网 时间:2024/05/19 18:13

  和多线程相关的基础知识,总共只有四个:pthread、NSThread、GCD和NSOperation。在前面,我们已经整理完了3个,现在就剩下NSOperation这一个了,下面,我们就继续学习这最后一个。

一、NSOpertion的基本使用

  
  NSOperation是一个抽象类,它本身并不具备封装操作的能力,必须使用它的子类。这一点和我们之前学过的UIGestureRecognizer类似。NSOperation有两个很重要的子类:NSInvocationOperationNSBlockOperation,它们都可以和NSOperationQueue配合来实现多线程编程,但是在实际用法上又不太一样。下面,我们就通过具体的代码示例来演示一下它们的使用和区别。

  1、NSInvocationOperation

  同为多线程技术,而且也都是基于OC,NSInvocationOperation和NSThread在使用上有很多相同的地方:1、都可以通过- initWithTarget: selector: object:创建线程对象并封装任务;2、创建完线程对象以后,都要调用- start方法来启动任务:

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // invocationOperation    [self invocationOperation];}// MARK:- NSInvocationOperation相关的代码- (void)invocationOperation {    // 创建NSInvocationOperation对象    NSInvocationOperation *invp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master) object:nil];    /**     *  initWithTarget:<#(nonnull id)#> selector:<#(nonnull SEL)#> object:<#(nullable id)#>     *  第一个参数 : 表示目标对象;     *  第二个参数 : 方法选择器;     *  第三个参数 : 可以通过这个参数给方法选择器中的方法传递参数,也可以为空。     */    // 开启任务    [invp start];}// MARK:- 封装的任务- (void)master {    // 打印当前任务和所在的线程    NSLog(@"任务---%@", [NSThread currentThread]);}

  先运行程序,看一下控制台打印情况:


领用NSInvocationOperation封装任务.png



  注意看一下线程的number,它并没有开子线程,还是在主线程中去执行任务。我们在前面说过,NSInvocationOperation要实现多线程编程,必须和NSOperationQueue配合使用。如果不是配合NSOperationQueue来执行相应的操作,不管你创建多少个对象,封装多少个任务,它都不会开线程。

  2、NSBlockOperation

  接下来再看一下NSBlockOperation的简单使用。和NSInvocationOperation所不同的是,NSBlockOperation创建对象和封装任务的过程就相对简单了,它有一个带block代码块的类方法,我们可以在创建对象的同时将任务封装进去:

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // blockOperation    [self blockOperation];}// MARK:- NSBlockOperation- (void)blockOperation {    // 创建NSBlockOperation对象,然后利用它类方法的block块封装任务    NSBlockOperation *blcp1 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务        NSLog(@"任务1---%@", [NSThread currentThread]);    }];    // 启动任务    [blcp1 start];    NSBlockOperation *blcp2 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务        NSLog(@"任务2---%@", [NSThread currentThread]);    }];    [blcp2 start];    NSBlockOperation *blcp3 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务        NSLog(@"任务3---%@", [NSThread currentThread]);    }];    [blcp3 start];}

  与NSInvocationOperation的用法类似,在创建完对象以后,要调用- start方法才能启动封装的任务。运行程序,看一下控制台打印:


使用NSBlockOperation封装任务.png

  从打印的情况来看,它也没有开子线程。原因还是没有结合NSOperationQueue来使用。不过,与NSInvocationOperation稍微有所不同的是,NSBlockOperation还有一个- addExecutionBlock:方法,我们可以在这个对象方法中追加任务:

// MARK:- NSBlockOperation- (void)blockOperation {    // 创建NSBlockOperation对象,然后利用它类方法的block块封装任务    NSBlockOperation *blcp1 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务        NSLog(@"blcp1---%@", [NSThread currentThread]);    }];    // 追加任务    [blcp1 addExecutionBlock:^{        // 追加任务1        NSLog(@"blcp1追加任务1---%@", [NSThread currentThread]);    }];    // 启动任务    [blcp1 start];    NSBlockOperation *blcp2 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务        NSLog(@"blcp2---%@", [NSThread currentThread]);    }];    // 追加任务    [blcp2 addExecutionBlock:^{        // 追加任务2        NSLog(@"blcp2追加任务1---%@", [NSThread currentThread]);    }];    [blcp2 start];    NSBlockOperation *blcp3 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务        NSLog(@"blcp3---%@", [NSThread currentThread]);    }];    // 追加任务    [blcp3 addExecutionBlock:^{        // 追加任务3        NSLog(@"blcp3追加任务1---%@", [NSThread currentThread]);    }];    [blcp3 addExecutionBlock:^{        // 追加任务3        NSLog(@"blcp3追加任务2---%@", [NSThread currentThread]);    }];    [blcp3 addExecutionBlock:^{        // 追加任务3        NSLog(@"blcp3追加任务3---%@", [NSThread currentThread]);    }];    [blcp3 start];}

  运行程序,仔细看一下控制台打印出来的信息,尤其要特别注意一下追加任务所在的线程:


利用- addExecutionBlock追加任务.png

  从控制台打印出的信息来看,如果同一个操作对象中不止一个任务,那么它肯定会开子线程,并且是并发执行。但是,这并不意味着一定是子线程!比如说,blcp3并不是追加的任务,但是它却是在子线程中执行的。而通过操作对象blcp3追加的任务2反倒是在主线程执行。总之,如果一个操作对象中的任务量大于1,那么它一定会开子线程并发执行。至于哪个任务是在子线程中执行,哪个任务是在主线程中执行,并不确定

二、NSOperationQueue

  
  在上面的实例中,我们分别演示了NSInvocationOperation和NSBlockOperation在没有结合NSOperationQueue时的使用情况。下面,我们就结合NSOperationQueue来使用一下,看看会发生什么事情。

  1、NSInvocationOperation和NSOperationQueue

  在学习GCD基础知识的过程中,我们知道,队列可以分为并发队列串行队列。获取这两种队列的具体操作如下:

并发队列:通过dispatch_queue_create( )函数传入DISPATCH_QUEUE_CONCURRENT参数,以及通过dispatch_get_global_queue( )函数可以得到一个并发队列;
  
串行队列:通过dispatch_queue_create( )函数传入DISPATCH_QUEUE_SERIAL参数,以及通过dispatch_get_main_queue( )函数可以得到一个串行队列。

  NSOperationQueue里面的队列和GCD中的队列稍微有所不同,它主要分为主队列非主队列。其中,通过[NSOperationQueue mainQueue]获取到的队列为主队列,也就是串行队列;而通过[[NSOperationQueue alloc] init]得到的队列为非主队列。并且,通过这种方式获取到的队列比较特殊,它同时具备并发和串行的特征。但是,它默认情况下是并发队列:

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // NSInvocationOperation和NSOperationQueue组合使用    [self invocationOperationWithQueue];}// MARK:- NSInvocationOperation和NSOperationQueue组合使用- (void)invocationOperationWithQueue {    // 创建NSInvocationOperation对象    NSInvocationOperation *invp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master) object:nil];    NSInvocationOperation *invp2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(alphaGo) object:nil];    // 创建一个主队列    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];  // 串行队列    // 创建非主队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];  // 同时具备并发和串行的功能;默认情况下是并发队列    // 将操作对象添加到队列中    [mainQueue addOperation:invp];  // 将invp添加到主队列中    [queue addOperation:invp2];  // 将invp2添加到非主队列中}// MARK:- 封装的任务- (void)master {    // 封装的任务    NSLog(@"master任务---%@", [NSThread currentThread]);}// MARK:- 再封装一个测试任务- (void)alphaGo {    // 封装的任务    NSLog(@"alphaGo任务---%@", [NSThread currentThread]);}

  在上面的代码中,我们创建了两种队列,并且分别封装了不同的任务。运行程序,注意看一下控制台打印出来线程的情况:


NSInvocationOperation配合NSOperationQueue来使用.png

  从上面的运行图中可以看出,添加到主队列中的任务依旧是在主线程中执行,而添加到非主队列中的任务默认是在子线程中执行。不过,这个还看不出到底是串行还是并发。接下来,我们就分别给它们封装多个任务进行测试。先来看主队列中添加多个任务的情况:

// MARK:- NSInvocationOperation和NSOperationQueue组合使用- (void)invocationOperationWithQueue {    // 创建NSInvocationOperation对象    NSInvocationOperation *mq1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master1) object:nil];    NSInvocationOperation *mq2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master2) object:nil];    NSInvocationOperation *mq3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master3) object:nil];    // 创建一个主队列    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];  // 串行队列    // 将操作对象添加到队列中    [mainQueue addOperation:mq1];  // 将mq1添加到主队列中    [mainQueue addOperation:mq2];    [mainQueue addOperation:mq3];}// MARK:- 封装的任务- (void)master1 {    // 封装的任务    NSLog(@"master1任务---%@", [NSThread currentThread]);}// MARK:- 封装的任务- (void)master2 {    // 封装的任务    NSLog(@"master2任务---%@", [NSThread currentThread]);}// MARK:- 封装的任务- (void)master3 {    // 封装的任务    NSLog(@"master3任务---%@", [NSThread currentThread]);}

  运行程序,看一下主队列中添加多个任务以后的执行情况:


主队列中添加多个任务以后的执行情况.png

  从打印的信息可知,主队列是不开线程的,并且多个任务之间串行执行。再来看一下非主队列中多任务的执行情况:

// MARK:- NSInvocationOperation和NSOperationQueue组合使用- (void)invocationOperationWithQueue {    // 创建NSInvocationOperation对象    NSInvocationOperation *mq1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master1) object:nil];    NSInvocationOperation *mq2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master2) object:nil];    NSInvocationOperation *mq3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(master3) object:nil];    // 创建非主队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];  // 同时具备并发和串行的功能;默认情况下是并发队列    [queue addOperation:mq1];  // 将q1添加到非主队列中    [queue addOperation:mq2];    [queue addOperation:mq3];}// MARK:- 封装的任务- (void)master1 {    // 封装的任务    NSLog(@"master1任务---%@", [NSThread currentThread]);}// MARK:- 封装的任务- (void)master2 {    // 封装的任务    NSLog(@"master2任务---%@", [NSThread currentThread]);}// MARK:- 封装的任务- (void)master3 {    // 封装的任务    NSLog(@"master3任务---%@", [NSThread currentThread]);}

  运行程序,然后点击模拟器屏幕,注意看一下线程和任务的执行顺序:


非主队列中多任务的执行情况.png

  从控制台打印出来的线程number和任务执行的顺序来看,非主队列会开子线程,并且多任务之间是并发执行的。

  2、NSBlockOperation和NSOperationQueue

  接下来,看一下NSInvocationOperation和NSOperationQueue结合使用的情况。因为主队列肯定是串行执行的,所以就没必要测试了。主要是看一下利用NSBlockOperation封装多个任务,然后再将其添加到非主队列中的执行情况:

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // NSBlockOperation和NSOperationQueue组合使用    [self blockOperationWithQueue];}// MARK:- NSBlockOperation和NSOperationQueue组合使用- (void)blockOperationWithQueue {    // 创建NSBlockOperation对象    NSBlockOperation *blck1 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"封装任务1---%@", [NSThread currentThread]);    }];    NSBlockOperation *blck2 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"封装任务2---%@", [NSThread currentThread]);    }];    NSBlockOperation *blck3 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"封装任务3---%@", [NSThread currentThread]);    }];    // 创建队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    // 将任务添加到队列中    [queue addOperation:blck1];    [queue addOperation:blck2];    [queue addOperation:blck3];}

  运行程序,然后点击模拟器屏幕。注意看一下控制台打印出来的线程的number和多任务之间执行的顺序:


NSBlockOperation和NSOperationQueue配合使用的情况.png

  很显然,当使用NSBlockOperation封装多个任务,并且将它们添加到非主队列时,系统会开子线程,并且多任务之间是并发执行的。和前面一样,我们再追加几个任务看一下:

// MARK:- NSBlockOperation和NSOperationQueue组合使用- (void)blockOperationWithQueue {    // 创建NSBlockOperation对象    NSBlockOperation *blck1 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"封装任务1---%@", [NSThread currentThread]);    }];    NSBlockOperation *blck2 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"封装任务2---%@", [NSThread currentThread]);    }];    NSBlockOperation *blck3 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"封装任务3---%@", [NSThread currentThread]);    }];    // 追加操作任务    [blck1 addExecutionBlock:^{        // 追加任务1        NSLog(@"blck1追加任务1---%@", [NSThread currentThread]);    }];    [blck1 addExecutionBlock:^{        // 追加任务2        NSLog(@"blck1追加任务2---%@", [NSThread currentThread]);    }];    [blck1 addExecutionBlock:^{        // 追加任务1        NSLog(@"blck1追加任务3---%@", [NSThread currentThread]);    }];    [blck2 addExecutionBlock:^{        // 追加任务1        NSLog(@"blck2追加任务1---%@", [NSThread currentThread]);    }];    [blck2 addExecutionBlock:^{        // 追加任务2        NSLog(@"blck2追加任务2---%@", [NSThread currentThread]);    }];    [blck2 addExecutionBlock:^{        // 追加任务1        NSLog(@"blck2追加任务3---%@", [NSThread currentThread]);    }];    // 创建队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    // 将任务添加到队列中    [queue addOperation:blck1];    [queue addOperation:blck2];    [queue addOperation:blck3];}

  运行程序,看一下控制台打印的消息:


追加多任务并将其添加到非主队列中.png

  从最终打印的情况来看,不管是直接添加到非主队列中的任务,还是后来追加的任务,它们都是在子线程中并发执行的。

  3、- addOperationWithBlock:方法

  其实,除了像上面这种通过操作对象来封装任务、创建队列,然后再将任务添加到队列中的做法之外,还要特别介绍一种简便的方法。- addOperationWithBlock:方法可以让你在创建完队列之后,直接将任务封装起来添加到队列中:

// MARK:- NSBlockOperation和NSOperationQueue组合使用- (void)blockOperationWithQueue {    // 创建队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    [queue addOperationWithBlock:^{        // 直接将任务封装起来添加到队列中        NSLog(@"封装任务1---%@", [NSThread currentThread]);    }];    [queue addOperationWithBlock:^{        // 直接将任务封装起来添加到队列中        NSLog(@"封装任务2---%@", [NSThread currentThread]);    }];    [queue addOperationWithBlock:^{        // 直接将任务封装起来添加到队列中        NSLog(@"封装任务3---%@", [NSThread currentThread]);    }];}

  - addOperationWithBlock:方法内部也有创建操作对象,然后再将任务添加到队列中去这些步骤。只不过,这些这些步骤现在不需要你去做,已经帮你做了,你只需要把任务封装进去就可以了。运行程序,看一下控制台打印出来的线程number和任务的执行顺序:


利用简便方法来封装任务.png

  从图中可以看出,系统有开线程,并且多个任务之间是并发执行的。

三、自定义NSOperation

  
  我们在之前的笔记中多次碰到过自定义类的情况,NSOperation也可以自定义,下面我们就简单的用一下。新建一个继承自NSOperation的ESOperation类,回到ViewController中包含它的头文件,然后实现如下代码:

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // 自定义NSOperation    [self customOperation];}// MARK:- 自定义NSOperation- (void)customOperation {    // 封装操作任务    ESOperation *op1 = [[ESOperation alloc] init];    ESOperation *op2 = [[ESOperation alloc] init];    // 创建队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    // 将任务添加到队列中    [queue addOperation:op1];    [queue addOperation:op2];}

  现在运行程序不会有任何反应,因为我们没有封装任何任务。自定义NSOperation要封装任务,必须重写- main方法。来到ESOperation.m文件中,重写- main方法:

// MARK:- 重写- main方法,执行相应的任务- (void)main {    // 将要封装任务的代码写在这里    NSLog(@"需要执行的任务---%@", [NSThread currentThread]);}

  运行程序,然后点击模拟器屏幕看一下:


自定义NSOperation.png

  上面的代码看着非常简单,而且和前面系统自带的NSInvocationOperation类,以及NSBlockOperation类比起来,好像也没有更方便,或者是更强大。那么,为什么要自定义NSOperation呢?原因主要有以下几点:

  1、在实际开发过程中,需要完成特定任务的代码量可能比较大,我们将这一部分代码单独抽出来,放在自定义类里面,可以有效减少控制器的代码量。说白了,就是专业的事情交给专门的类去做,让代码结构和功能更清晰合理
  2、特定的任务放在独立的类中,可以有效提高代码的复用率
  3、外界只需要知道某个类怎么使用就可以了,不必关心它内部的实现细节,从而有效提升开发效率

  NSOperation基础知识部分的笔记暂时先整理到这里,后面会接着整理NSOperation更高级的用法。详细代码参见ESNSOperationEXercise。

原创粉丝点击