NSOperation基础知识进阶

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

  我们在上一篇笔记中整理了NSOperation的一些基础知识,接下来,我们进一步学习它的高级用法。

一、NSOperation进阶

  1、队列的最大并发数

  我们在讲NSOperationQueue时说过,通过[[NSOperationQueue alloc] init]这种方式得到的队列,它同时具备并发和串行的特征。那么,它在什么时候是并发,什么时候是串行呢?可以按住command键点击进入NSOperationQueue的头文件,它里面有一个很重要的属性能帮助我们控制当前最大的并发数:


maxConcurrentOperationCount.png

  maxConcurrentOperationCount的返回值是一个NSInteger类型,并且它不能为0。当它的返回值为1时,队列中所有的任务都是串行执行的。不过,一定要注意,串行执行并不等于只开一条线程;当它的值大于1时,那么队列就是并发队列。而且,我们也看到了,NSOperationQueueDefaultMaxConcurrentOperationCount也就是最大并发数,它的默认值是-1。-1在计算机中往往有着特殊的含义,表示其值不受限制。

  2、队列暂停、恢复和取消

  
  在NSOperationQueue的头文件中继续往下看,它里面有一个suspended属性,以及一个- cancelAllOperations方法:


队列的暂停和取消.png

  我们可以通过suspend属性来暂停或者恢复队列中的任务。不过,需要注意的是,只能暂停队列中排队等待的任务,不能停止正在执行中的任务。另外,还可以通过- cancelAllOperations方法取消队列中所有的任务,并且,任务一旦取消便不可再恢复。- cancelAllOperations方法的应用场景非常广泛,在自定义NSOperation的时候一定要特别注意,不管- main方法里面有多少个耗时操作,实际上它都只是一个任务,如果要赋予它取消功能,就必须通过cancel属性来判断:

- (void)main {    // 耗时操作1    for (int i = 0; i < 1000; i++) {        // 打印        NSLog(@"耗时操作1执行了%d次---%@", i, [NSThread currentThread]);    }    // 判断当前操作是否取消,如果是,就直接返回    if (self.isCancelled) {        // 直接返回        return;    }    // 耗时操作2    for (int i = 0; i < 1000; i++) {        // 打印        NSLog(@"耗时操作2执行了%d次---%@", i, [NSThread currentThread]);    }    // 判断当前操作是否取消,如果是,就直接返回    if (self.isCancelled) {        // 直接返回        return;    }    // 耗时操作2    for (int i = 0; i < 1000; i++) {        // 打印        NSLog(@"耗时操作3执行了%d次---%@", i, [NSThread currentThread]);    }}

  - cancelAllOperations方法内部会调用cancel属性的getter方法。在- main方法的耗时操作后面加入取消判断,一但外面有调用- cancelAllOperations方法,那么- main方法里面封装的任务就会被及时取消。

二、NSOperation操作依赖和监听

  
  1、添加操作依赖

  我们之前在讲GCD的时候接触过栅栏函数,也就是说通过相应的手段来控制并发队列中任务执行的顺序。在NSOperation中也有类似这样的概念,也就是通过- addDependency:方法来添加操作依赖。其使用方法非常简单,比如说,A和B分别是已经封装好任务的操作对象,[A addDependency:B]的意思是,在操作对象B执行完毕以后,才能执行操作对象A:

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // blockOperation    [self blockOperation];}// MARK:- NSBlockOperation的高级用法- (void)blockOperation {    // 创建队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    // 封装操作    NSBlockOperation *blcp1 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"任务1---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp2 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务2        NSLog(@"任务2---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp3 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务3        NSLog(@"任务3---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp4 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务4        NSLog(@"任务4---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp5 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务5        NSLog(@"任务5---%@", [NSThread currentThread]);    }];    // 添加操作依赖    [blcp3 addDependency:blcp1];  // blcp3依赖于blcp1    [blcp1 addDependency:blcp5];  // blcp1依赖于blcp5    [blcp5 addDependency:blcp2];  // blcp5依赖于blcp2    [blcp2 addDependency:blcp4];  // blcp2依赖于blcp4    // 将所有的任务添加到队列中    [queue addOperation:blcp1];    [queue addOperation:blcp2];    [queue addOperation:blcp3];    [queue addOperation:blcp4];    [queue addOperation:blcp5];}

  通过前面的学习,我们知道,在没有添加操作依赖的情况下,上面代码中的任务执行顺序是不确定的。但是,添加完上面的操作依赖之后,任务的执行顺序应该是:blcp4→blcp2→blcp5→blcp1→blcp3。运行程序,看一下是不是这样的:


添加完操作依赖之后任务的执行顺序.png

  从上面的运行图可以看出,任务执行的顺序完全符合我们的预期。不过,在添加操作依赖的时候千万要注意,就是一定不能搞成循环依赖(也就是你依赖我,我依赖你)!出现循环依赖虽然不会引发崩溃,但是程序运行以后无响应。比如说,像上面的代码,若是在添加操作依赖代码的最后,再加一句[blcp4 addDependency:blcp3],那么就形成循环依赖了。

  其实,添加操作依赖这个方法的功能非常强大。除了在一个队列中可以添加操作依赖之外,多个队列和不同的任务之间也可以添加操作依赖:

// MARK:- NSBlockOperation的高级用法- (void)blockOperation {    // 创建队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];    // 封装操作    NSBlockOperation *blcp1 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"任务1---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp2 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务2        NSLog(@"任务2---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp3 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务3        NSLog(@"任务3---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp4 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务4        NSLog(@"任务4---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp5 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务5        NSLog(@"任务5---%@", [NSThread currentThread]);    }];    // 添加操作依赖    [blcp3 addDependency:blcp1];    [blcp1 addDependency:blcp5];    [blcp5 addDependency:blcp2];    [blcp2 addDependency:blcp4];//    [blcp4 addDependency:blcp3];  // 循环依赖,运行程序以后无响应    // 将所有的任务添加到队列中    [queue addOperation:blcp1];    [queue2 addOperation:blcp2];  // 将blcp2添加到queue2中    [queue addOperation:blcp3];    [queue2 addOperation:blcp4];  // 将blcp4添加到queue2中    [queue addOperation:blcp5];}

  在上面的代码中,我们新创建了一个队列queue2,并且将blcp2和blcp4添加到queue2中,其它的操作任然放在queue中,但是,程序运行以后,其结果依然和上面在一个队列中添加操作依赖时一样:


多个队列和不同的人物之间也可以添加操作依赖.png

  2、监听任务的执行

  通常情况下,为了提高用户体验,在某个任务执行完毕以后,要及时的发出通知。因此,必须对任务的执行情况进行监听。在NSOperation中监听任务的执行,可以通过completionBlock属性来实现:

// MARK:- NSBlockOperation的高级用法- (void)blockOperation {    // 创建队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];    // 封装操作    NSBlockOperation *blcp1 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务1        NSLog(@"任务1---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp2 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务2        NSLog(@"任务2---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp3 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务3        NSLog(@"任务3---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp4 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务4        NSLog(@"任务4---%@", [NSThread currentThread]);    }];    NSBlockOperation *blcp5 = [NSBlockOperation blockOperationWithBlock:^{        // 封装任务5        NSLog(@"任务5---%@", [NSThread currentThread]);    }];    // 监听任务的执行    [blcp3 setCompletionBlock:^{        // 操作完成以后发出通知        NSLog(@"blcp3执行完毕---%@!", [NSThread currentThread]);    }];    blcp5.completionBlock = ^ {        // 在blcp5执行完毕以后做一些事情        NSLog(@"blcp5执行完毕---%@!", [NSThread currentThread]);    };    // 添加操作依赖    [blcp3 addDependency:blcp1];    [blcp1 addDependency:blcp5];    [blcp5 addDependency:blcp2];    [blcp2 addDependency:blcp4];    // 将所有的任务添加到队列中    [queue addOperation:blcp1];    [queue2 addOperation:blcp2];  // 将blcp2添加到queue2中    [queue addOperation:blcp3];    [queue2 addOperation:blcp4];  // 将blcp4添加到queue2中    [queue addOperation:blcp5];}

  通过completionBlock属性来封装一段代码,一旦某个操作任务执行完毕之后,它就会调用这个block代码块中的代码:


监听NSOperation中任务的执行情况.png

  需要说明的是,监听任务执行的block代码块和被监听的任务,它们并不一定在同一个线程中执行,它们是并发执行的。

三、NSOperation线程间的通信

  
  我们在前面的笔记中讲NSThread和GCD时,都有讲过线程间的通信,下面,我们就来看一下在NSOperation中如何实现线程间的通信。

  回顾一下GCD线程间通信的相关知识,我们是使用嵌套block的方式来实现线程间通信的。在NSOperation中,也可以用嵌套block的方式来实现线程间的通信:

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // 点击屏幕下载图片    [self downloadImage];}// MARK:- 演示线程间的通信- (void)downloadImage {    // 创建一个非主队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    // 封装任务    NSBlockOperation *downloadImage = [NSBlockOperation blockOperationWithBlock:^{        // 确定URL地址        NSURL *url = [NSURL URLWithString:@"http://i5qiniu.mtime.cn/pi/2016/12/21/102726.51910822_1000X1000.jpg"];        // 下载图片的二进制数据        NSData *imageData = [NSData dataWithContentsOfURL:url];        // 将二进制数据转换为图片        UIImage *image = [UIImage imageWithData:imageData];        NSLog(@"下载图片---%@", [NSThread currentThread]);        // 刷新UI        [[NSOperationQueue mainQueue] addOperationWithBlock:^{            // 将图片设置到UIImageView控件上去            self.imageView.image = image;            NSLog(@"刷新UI---%@", [NSThread currentThread]);        }];    }];    // 将任务添加到队列中    [queue addOperation:downloadImage];}

  首先,我们使用了一个非主队列,以确保任务在执行过程中会开子线程;其次,使用NSBlockOperation来封装任务,并且在这个block代码块中嵌套block,以便回到主线程中去设置图片并刷新UI。运行程序,看一下实现效果:


NSOperation线程间的通信演示.gif

  从控制台打印出来的线程number来看,我们是在子线程中下载图片的,完了之后再回到主线程中去刷新UI,完成了子线程和主线程之间的通信。

  我们在讲《GCD知识进阶》的时候做过一个拼接下载图片的案例,接下来我们用NSOperation的相关知识再做一遍这个实例,进一步强化进程间通信的先关知识。

// MARK:- 点击屏幕执行相应的操作- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    // 点击屏幕下载图片    [self downloadImages];}// MARK:- 下载两张图片,然后再将它们合成一张- (void)downloadImages {    // 创建一个非主队列    NSOperationQueue *queue = [[NSOperationQueue alloc] init];    // 下载图片1    NSBlockOperation *downloadImageOne = [NSBlockOperation blockOperationWithBlock:^{        // 确定图片的URL地址        NSURL *urlOne = [NSURL URLWithString:@"http://img05.tooopen.com/images/20160128/tooopen_sy_155633976973.jpg"];        // 下载图片的二进制数据        NSData *imageDataOne = [NSData dataWithContentsOfURL:urlOne];        // 将二进制数据转换为图片        UIImage *imageOne = [UIImage imageWithData:imageDataOne];        // 将下载完的图片imageOne保存到imageOne属性中        self.imageOne = imageOne;        NSLog(@"下载图片1的线程为:%@", [NSThread currentThread]);    }];    // 下载图片2    NSBlockOperation *downloadImageTwo = [NSBlockOperation blockOperationWithBlock:^{        // 确定图片的URL地址        NSURL *urlTwo = [NSURL URLWithString:@"http://img02.tooopen.com/images/20151127/tooopen_sy_149698679682.jpg"];        // 下载图片的二进制数据        NSData *imageDataTwo = [NSData dataWithContentsOfURL:urlTwo];        // 将二进制数据转换为图片        UIImage *imageTwo = [UIImage imageWithData:imageDataTwo];        // 将下载完的图片imageTwo保存到imageTwo属性中        self.imageTwo = imageTwo;        NSLog(@"下载图片2的线程为:%@", [NSThread currentThread]);    }];    // 合成图片    NSBlockOperation *spliceImages = [NSBlockOperation blockOperationWithBlock:^{        // 确定绘图区域大小        CGSize imageSize = CGSizeMake(self.view.frame.size.width * 0.8, self.view.frame.size.height * 0.8);        // 开启图形上下文        UIGraphicsBeginImageContext(imageSize);        // 将第一张图片画到绘图区域的上半部分        [self.imageOne drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height * 0.5)];        // 清空imageOne        self.imageOne = nil;  // 图片绘制到图形上下文以后就没用了,为了节省空间,需要将它清除        // 将第二张图片画到绘图区域的下半部分        [self.imageTwo drawInRect:CGRectMake(0, imageSize.height * 0.5, imageSize.width, imageSize.height * 0.5)];        // 清空imageTwo        self.imageTwo = nil;        // 根据图形上下文获取一张绘好的图片        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();        // 关闭图形上下文        UIGraphicsEndImageContext();        // 回到主线程中刷新UI        [[NSOperationQueue mainQueue] addOperationWithBlock:^{            // 刷新UI            self.imageView.image = image;            NSLog(@"刷新UI的线程为:%@", [NSThread currentThread]);        }];    }];    // 添加操作依赖    [downloadImageTwo addDependency:downloadImageOne];    [spliceImages addDependency:downloadImageTwo];    // 将任务添加到队列中    [queue addOperation:downloadImageOne];    [queue addOperation:downloadImageTwo];    [queue addOperation:spliceImages];}

  上面的代码,最关键的是在添加依赖。只有当两张图片都下载完成了,才有可能把它们合成一张。运行程序,注意看一下控制台打印出来的线程number:


下载多张图片、添加操作依赖、回到主线程刷新UI.gif

  以上就是NSOperation基本应用的相关知识,后面的笔记中会接着整理。相关的代码参见NSOperationAdvanced。