iOS开发项目化经典教程
上QQ阅读APP看书,第一时间看更新

1.4 NSOperation和NSOperationQueue

前面已经介绍了实现多线程的几种技术,除了PThread、NSThread、GCD之外,还有一种非常简单的多线程实现方式,就是NSOperation和NSOperationQueue。

NSOpration和NSOperationQueue的实现方式与GCD类似,这是因为它是基于GCD来实现的,不过相比较于GCD而言,NSOpration和NSOperationQueue使用的是OC语言,操作更加面向对象,因此更加容易使用。接下来,本节将针对NSOperation类和NSOperation Queue类的相关内容进行详细的讲解。

1.4.1 NSOperation简介

NSOperation类的实例代表一个多线程任务,这个实例封装了需要执行的操作和执行操作所需的数据,并且能够以并发或非并发的方式执行这个操作。

NSOperation类本身是一个抽象类,一般用于定义子类公用的方法和属性。为了得知任务当前的状态,NSOperation类提供了4个属性来判断,用于回馈它的状态变化,它们的定义格式如下:

 @property (readonly, getter=isCancelled) BOOL cancelled;  // 取消 
 @property (readonly, getter=isExecuting) BOOL executing;  // 运行 
 @property (readonly, getter=isFinished) BOOL finished;    // 结束 
 @property (readonly, getter=isReady) BOOL ready;          // 就绪   

开发者开发时必须处理添加操作的状态,这些状态都是基于KVO通知决定的,所以开发者想要手动改变添加操作的状态时,必须要手动发送通知。这4个属性都是相互独立的,每个时刻只可能有一个状态是YES。其中,finished在操作完成后需要及时设置为YES,这是因为NSOperationQueue所管理的队列中,只有isFinished为YES时才会将该任务从队列中移除,这点在内存管理的时候非常关键,同时这样做也可以有效地避免死锁。

除此之外,NSOperation类还提供了一些常用的方法,用于执行它的实例的操作,如表1-1所示。

表1-1 NSOperation类的常用方法

0

表1-1列举了NSOperation类一些常见的方法,由表可知,这些方法根据功能的不同,大致可以分为以下操作。

1. 执行操作

要想执行一个NSOperation对象,可以通过如下两种方式实现。

(1)第1种是手动地调用start这个方法,这个方法一旦调用,就会在当前调用的线程进行同步执行,因此在主线程中一定要谨慎调用,否则会把主线程阻塞。

(2)第2种是将NSOperation添加到NSOperationQueue中,这是开发者使用最多且被提倡的方法,NSOperationQueue会在NSOperation被添加进去的时候尽快执行,并且实现异步执行。

总而言之,如果只是想将任务实现同步执行,只需要重写main方法,在其内部添加相应的操作;如果想要将任务异步执行,则需要重写start方法,同时让isConcurrent方法返回YES。当把任务添加进NSOperationQueue中时,系统将自动调用重写的这个start方法,这时将不再调用main里面的方法。

2. 取消操作

当操作开始执行之后,默认会一直执行直到完成,但是也可以调用cancel方法中途取消操作的执行。当然,这个操作并非常见的取消,实质上取消操作是按照如下方式作用的:

如果这个操作在队列中没有执行,而这个时候取消这个操作,并将状态finished设置为YES,那么这时的取消就是直接取消了;如果这个操作已经在执行,则只能等待这个操作完成,调用cancel方法也只是将isCancelled的状态设置为YES。

因此,开发者应该在每个操作开始前,或者在每个有意义的实际操作完成后,先检查下这个属性是不是已经设置为YES。如果是YES,则后面的操作都可以不必再执行了。

3. 添加依赖

NSOperation中可以将操作分解为若干个小任务,通过添加它们之间的依赖关系进行操作,就可以对添加的操作设置优先级。例如,最常用的异步加载图片,第1步是通过网络加载图片,第2步可能需要对图片进行处理(调整大小或者压缩保存)。当前的操作通过调用addDependency:方法,可以协调好相应的先后关系。

特别需要注意的是,两个任务间不能添加相互依赖,如A依赖B,同时B又依赖A,这样就会导致死锁。在每个操作完成时,需要将isFinished设置为YES,不然后续的操作是不会开始执行的。

4. 监听操作

如果想在一个NSOperation执行完毕后做一些事情,就调用setCompletionBlock:方法来设置想做的事情。

1.4.2 NSOperationQueue简介

NSOperationQueue类的实例代表一个队列,与GCD中的队列一样,同样是先进先出的,它负责管理系统提交的多个NSOperation对象,NSOperationQueue底层维护一个线程池,会按照NSOperation对象添加到队列中的顺序来启动相应的线程。

NSOperationQueue负责管理、执行所有的NSOperation对象,这个对象会由线程池中的线程负责执行。为此,NSOperationQueue提供了一些常见的方法,用于操作队列,如表1-2所示。

表1-2 NSOperationQueue类的常用方法

0

表1-2列举了NSOperationQueue类一些常见的方法,由表可知,这些方法根据功能的不同,大致可以分为以下操作。

1. 添加NSOperation到NSOperationQueue中

要想执行任务,需要将要执行的NSOperation对象添加到NSOperationQueue中,由其内部的线程池管理调度。若要添加单个NSOperation对象,可以通过addOperation:方法实现;若想要添加多个NSOperation对象到同一个NSOperationQueue队列中,可以调用如下方法。

 - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait   

从上述代码看出,该方法包含2个参数。其中,第2个参数wait如果设置为YES,将会阻塞当前线程,直到提交的全部NSOperation对象执行完毕后释放;如果设置为NO,该方法会立即返回,NSArray所包含的NSOperation对象将以异步的方式执行,不会阻塞当前线程。

另外,还可以通过block代码块的形式来添加NSOperation对象,可以调用如下方法。

 - (void)addOperationWithBlock:(void (^)(void))block   

通常情况下,NSOperation对象添加到队列之后,短时间内就会得到运行。如果多个任务间存在依赖,或者整个队列被暂停,则可能需要等待。

需要注意的是,NSOperation对象一旦添加到NSOperationQueue之后,绝对不要再修改NSOperation对象的状态。由于NSOperation对象可能会在任何时候运行,改变它的依赖或数据会产生不利的影响。因此,开发者只能查看NSOperation对象的状态,如是否正在运行、等待运行、已经完成等。

2. 修改NSOperation对象的执行顺序

对于添加到队列中的NSOperation对象,它们的执行顺序取决于以下两点。

(1)查看NSOperation对象是否已经就绪,这个是由对象的依赖关系确定的。

(2)根据所有NSOperation对象的相对优先级来确定执行顺序。

为此,NSOperation类提供了queuePriority属性,用于改变添加到队列中的NSOperation对象的优先级,定义格式如下:

 @property NSOperationQueuePriority queuePriority;   

从上述代码看出,该属性是一个NSOperationQueuePriority类型的变量,这是一个枚举类型。优先级等级由低到高,其表示的意义如下。

(1)NSOperationQueuePriorityVeryLow:非常低。

(2)NSOperationQueuePriorityLow:低。

(3)NSOperationQueuePriorityNormal:一般。

(4)NSOperationQueuePriorityHigh:高。

(5)NSOperationQueuePriorityVeryHigh:非常高。

需要注意的是,优先级只能应用于相同队列中的NSOperation对象,如果应用有多个队列,那么不同队列之间的NSOperation对象的优先级等级是互相独立的。因此,不同队列中优先级低的操作仍然可能比优先级高的操作更早执行。

另外,优先级是不能替代依赖关系的,优先级只是对已经准备好的NSOperation对象确定执行顺序。在执行中先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行。

3. 设置或者获取队列的最大并发操作数量

当队列中的线程过多时,显然也会影响到应用程序的执行效率。通过设置队列的最大并发操作数量,可以约束队列中的线程的个数,这样就可以设置队列中最多支持多少个并发线程。

虽然NSOperationQueue类设计用于并发执行操作,但是也可以强制单个队列一次只能执行一个操作。maxConcurrentOperationCount可以配置队列的最大并发操作数量,定义格式如下所示:

 @property NSInteger maxConcurrentOperationCount;   

当maxConcurrentOperationCount设为1时,就表示队列每次只能执行一个操作,但是串行化的NSOperationQueue并不等同于GCD中的串行Dispatch Queue。

4. 等待NSOperation操作执行完成

在实际开发中,为了优化应用的性能,开发者应该尽可能将应用设计为异步操作,让应用在操作正在执行时可以去处理其他事情。如果需要在当前线程中处理操作之前插入其他操作,通过NSOperation类的waitUntilFinished方法来阻塞当前线程,示例如下:

 // 会阻塞当前线程,等到某个NSOperation执行完毕   
 [operation waitUntilFinished];     

从上述代码看出,operation表示一个操作,该操作会等到其余的某个操作执行完毕后再执行。但是应该注意避免编写这样的代码,这不仅影响整个应用的并发性,而且也降低了用户的体验。

另外,NSOperationQueue类提供了一个方法,用于表示某个操作可以在执行的同时等待一个队列中的其他全部操作,示例如下:

 // 阻塞当前线程,等待queue的所有操作执行完毕   
 [queue waitUntilAllOperationsAreFinished];     

需要注意的是,在等待一个queue时,应用的其他线程仍然可以向队列中添加其他操作,因此可能会加长线程的等待时间。绝对不要在应用的主线程中等待一个或者多个NSOperation,而要在子线程中进行等待,否则主线程阻塞将会导致应用无法响应用户事件,应用也将表现为无响应。

5. 暂停和继续NSOperationQueue队列

开发者如果想临时暂停NSOperationQueue队列中所有的NSOperation操作的执行,可以将sus pended属性设置为YES,定义格式如下:

 @property (getter=isSuspended) BOOL suspended;   

需要注意的是,暂停一个NSOperationQueue队列不会导致正在执行的NSOperation操作在中途暂停,只是简单地阻止调度新的NSOperation操作执行。可以在响应用户请求时,暂停一个NSOperation操作来暂停等待中的任务,然后根据用户的请求,再次设置suspended属性为NO来继续NSOperationQueue队列中的操作执行。

综上所述,将NSOperation和NSOperationQueue这两个类结合使用,就能够实现多线程,大体分为如下4个步骤。

(1)将需要执行的操作封装到一个NSOperation对象中。

(2)将NSOperation对象添加到NSOperationQueue对象中。

(3)系统自动将NSOperationQueue对象中的NSOperation对象取出来。

(4)将取出的NSOperation对象封装的操作放到一个新线程中执行。

1.4.3 使用NSOperation子类操作

因为NSOperation本身是抽象基类,表示一个独立的计算单元,因此如果要创建对象的话,必须使用它的子类。Foundation框架提供了两个具体子类直接供开发者使用,它们就是NSInvocationOperation和NSBlockOperation类。除此之外,还可以自定义子类,只要继承于NSOperation类,实现内部相应的方法即可。针对这3种情况的详细讲解如下。

1. NSInvocationOperation

NSInvocationOperation类用于将特定对象的特定方法封装成NSOperation,基于一个对象和selector来创建操作。如果已经有现有的方法来执行需要的任务,就可以使用这个类。

接下来,新建一个single View Application工程,命名为09-NSInvocationOperation,具体代码如例1-13所示。

【例1-13】ViewController.m

  1   #import "ViewController.h" 
  2   @interface ViewController () 
  3   @end 
  4   @implementation ViewController 
  5   - (void)viewDidLoad 
  6   { 
  7      [super viewDidLoad]; 
  8      // 创建操作 
  9      NSInvocationOperation *operation = [[NSInvocationOperation alloc] 
  10     initWithTarget:self selector:@selector(download) object:nil]; 
  11     // 创建队列 
  12     NSOperationQueue *queue = [[NSOperationQueue alloc] init];  
  13     // 添加操作到队列中,会自动异步执行 
  14     [queue addOperation:operation]; 
  15  } 
  16  - (void)download     
  17  { 
  18     NSLog(@"download-----%@", [NSThread currentThread]); 
  19  } 
  20  @end   

运行程序,结果如图1-39所示。

0

图1-39 程序的运行结果

从图1-39中看出,第14行调用addOperation:方法让操作异步执行。若要使操作同步执行,则将第14行代码改为调用start方法开启即可。

2. NSBlockOperation

NSBlockOperation类用于将代码块封装成NSOperation,能够并发地执行一个或者多个block对象,所有相关的block代码块都执行完之后,操作才算完成。

接下来,新建一个single View Application工程,命名为10-NSBlockOperation,具体代码如例1-14所示。

【例1-14】ViewController.m

  1   #import "ViewController.h" 
  2   @interface ViewController () 
  3   @end 
  4   @implementation ViewController 
  5   - (void)viewDidLoad 
  6   { 
  7      [super viewDidLoad]; 
  8      NSBlockOperation *operation = [[NSBlockOperation alloc] init]; 
  9      [operation addExecutionBlock:^{ 
  10        NSLog(@"---下载图片----1---%@", [NSThread currentThread]); 
  11     }]; 
  12     [operation addExecutionBlock:^{ 
  13        NSLog(@"---下载图片----2---%@", [NSThread currentThread]); 
  14     }]; 
  15     [operation addExecutionBlock:^{ 
  16        NSLog(@"---下载图片----3---%@", [NSThread currentThread]); 
  17     }]; 
  18     [operation start]; 
  19  } 
  20  @end   

运行程序,结果如图1-40所示。

0

图1-40 程序的运行结果

从图1-40中看出,尽管operation对象是通过调用start方法来开启线程的,但是operation添加的3个block是并发执行的,也就是在不同线程中执行的。因此,当同一个操作中的任务量大于1时,该操作会实现异步执行。

3. 自定义NSOperation子类

如果NSInvocationOperation和NSBlockOperation对象不能满足需求,开发者可以自定义操作来直接继承NSOperation,并添加任何想要的行为,这要取决于需要自定义的类是想要实现非并发还是并发的NSOperation。

定义非并发的NSOperation要简单许多,只需要重载main这个方法,在这个方法里面执行主任务,并正确地响应取消事件。对于并发NSOperation操作,必须重写NSOperation的多个基本方法进行实现。

1.4.4 实战演练——自定义NSOperation子类下载图片

前面已经介绍了NSOperation的两个子类,使用极其方便。如果这两个子类无法满足需求,我们可以自定义一个继承自NSOperation的类。接下来,通过一个下载图片的案例,讲解如何使用自定义的NSOperation子类,这里暂时先介绍非并发的NSOperation,具体内容如下。

(1)新建一个single View Application工程,命名为“11-CustomNSOperation”。

(2)进入Main.StoryBoard,从对象库拖曳1个Image View到程序界面,设置Image View的Mode模式为Center,设置一个背景颜色,并且用拖曳的方式对这个控件进行属性声明。

(3)新建一个类DownloadOperation,继承于NSOperation类,表示下载操作。在DownloadOperation.m文件中,重写main方法,并且为该自定义类创建一个代理,并为该代理提供一个下载图片的方法,DownloadOperation类的声明和实现文件如例1-15和例1-16所示。

【例1-15】DownloadOperation.h

  1   #import <Foundation/Foundation.h> 
  2   #import <UIKit/UIKit.h> 
  3   @class DownloadOperation; 
  4   // 定义代理 
  5   @protocol DownloadOperationDelegate <NSObject> 
  6   // 下载操作方法 
  7   - (void)downloadOperation:(DownloadOperation *)operation  
  8   image:(UIImage *)image; 
  9   @end 
  10  @interface DownloadOperation : NSOperation  
  11  // 需要传入图片的URL 
  12  @property (nonatomic,strong) NSString *url; 
  13  // 声明代理属性 
  14  @property (nonatomic,weak) id<DownloadOperationDelegate> delegate; 
  15  @end   

【例1-16】DownloadOperation.m

  1   #import "DownloadOperation.h" 
  2   @implementation DownloadOperation 
  3   - (void)main 
  4   { 
  5      @autoreleasepool { 
  6         // 获取下载图片的URL 
  7         NSURL *url = [NSURL URLWithString:self.url]; 
  8         // 从网络下载图片 
  9         NSData *data = [NSData dataWithContentsOfURL:url]; 
  10        // 生成图像 
  11        UIImage *image = [UIImage imageWithData:data]; 
  12        // 在主操作队列通知调用方更新UI 
  13        [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 
  14           NSLog(@"图片下载完成......"); 
  15           if ([self.delegate respondsToSelector: 
  16           @selector(downloadOperation:image:)]) { 
  17              [self.delegate downloadOperation:self image:image]; 
  18           } 
  19        }]; 
  20     } 
  21  } 
  22  @end   

在例1-16中,main方法实现了下载操作的功能,并通过downloadOperation:image:方法将下载好的图片通过代理的方式传递给代理方。

(4)在ViewController.m文件中,创建NSOperationQueue队列,设置ViewController成为DownloadOperation的代理对象,创建自定义操作,并将自定义操作对象添加到NSOperationQueue队列中,最后刷新界面,如例1-17所示。

【例1-17】ViewController.m

  1   #import "ViewController.h" 
  2   #import "DownloadOperation.h" 
  3   @interface ViewController ()<DownloadOperationDelegate> 
  4   @property (weak, nonatomic) IBOutlet UIImageView *imageView; 
  5   @end   
  6   @implementation ViewController 
  7   - (void)viewDidLoad { 
  8      [super viewDidLoad]; 
  9      // 创建队列 
  10     NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 
  11     // 队列添加操作 
  12     DownloadOperation *operation = [[DownloadOperation alloc] init]; 
  13     operation.delegate = self; 
  14     operation.url = @"http://www.itcast.cn/images/logo.png"; 
  15     // 将下载操作添加到操作队列中去 
  16     [queue addOperation:operation]; 
  17  } 
  18     // 执行下载操作 
  19  - (void)downloadOperation:(DownloadOperation *)operation image:(UIImage *)image 
  20  { 
  21     self.imageView.image = image; 
  22  } 
  23  @end  

运行程序,运行结果如图1-41所示。

0

图1-41 程序的运行结果

从图1-41中可以看出,自定义的NSOperation的子类同样实现了图片下载操作。

1.4.5 实战演练——对NSOperation操作设置依赖关系

一个队列中执行任务的先后顺序是不一样的,如果队列的操作是并发执行的,则会创建多个线程,每个操作的优先级更是不固定。通过任务间添加依赖,可以为任务设置执行的先后顺序。为了大家更好地理解,接下来,通过一个案例来演示设置依赖的效果。

新建一个single View Application工程,命名为“12-NSOperationAddDependency”。进入ViewController.m文件,通过一个模拟演示,讲解如何对操作设置依赖关系,代码如例1-18所示。

【例1-18】ViewController.m

  1   #import "ViewController.h" 
  2   @interface ViewController () 
  3   @end 
  4   @implementation ViewController 
  5   - (void)viewDidLoad { 
  6      [super viewDidLoad]; 
  7      // 创建队列 
  8      NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 
  9      // 创建操作 
  10     NSBlockOperation *operation1 = [NSBlockOperation  
  11     blockOperationWithBlock:^(){ 
  12        NSLog(@"执行第1次操作,线程:%@", [NSThread currentThread]); 
  13     }]; 
  14     NSBlockOperation *operation2 = [NSBlockOperation  
  15     blockOperationWithBlock:^(){ 
  16        NSLog(@"执行第2次操作,线程:%@", [NSThread currentThread]); 
  17     }]; 
  18     NSBlockOperation *operation3 = [NSBlockOperation  
  19     blockOperationWithBlock:^(){ 
  20        NSLog(@"执行第3次操作,线程:%@", [NSThread currentThread]); 
  21     }]; 
  22     // 添加依赖 
  23     [operation1 addDependency:operation2]; 
  24     [operation2 addDependency:operation3]; 
  25     // 将操作添加到队列中去 
  26     [queue addOperation:operation1]; 
  27     [queue addOperation:operation2]; 
  28     [queue addOperation:operation3]; 
  29  } 
  30  @end   

在例1-18中,第23~24行代码通过对操作设置依赖来改变操作的执行顺序,按照此依赖关系,最先执行operation3,然后执行operation2,最后执行operation1。运行两次程序,两次的运行结果如图1-42和图1-43所示。

0

图1-42 程序运行的第1次结果

0

图1-43 程序运行的第2次结果

从图1-42和图1-43中可以看出,队列中的操作执行的先后顺序,确实是按照最先执行operation3,然后执行operation2,最后执行operation1的顺序来的,说明给操作添加依赖关系可以很好地设置操作执行的先后顺序。

1.4.6 实战演练——模拟暂停和继续操作

表视图开启线程下载远程的网络界面,滚动页面时势必会有影响,降低用户的体验。针对这种情况,当用户滚动屏幕的时候,暂停队列;用户停止滚动的时候,继续恢复队列。为了大家更好地理解,接下来,通过一个案例,演示如何暂停和继续操作,具体内容如下。

(1)新建一个single View Application工程,命名为“13-SuspendAndContinue”。

(2)进入Main.StoryBoard,从对象库拖曳3个Button到程序界面,分别设置Title为“添加”“暂停”和“继续”,并且用拖曳的方式给这3个控件进行单击响应的声明,分别对应着添加操作、暂停操作、继续操作。

(3)进入ViewController.m文件,在单击“添加”按钮后激发的方法中,首先设置操作的最大并发操作数为1,向创建的队列中添加20个操作,然后为线程设置休眠时间为1.0s,相当于GCD的异步串行操作。

(4)当队列中的操作正在排队时,则将调用setSuspended:方法传入YES参数将其挂起;当队列中的操作被挂起的时候,则调用setSuspended:方法传入NO参数让它们继续排队,代码如例1-19所示。

【例1-19】ViewController.m

  1   #import "ViewController.h" 
  2   @interface ViewController () 
  3   @property (nonatomic,strong) NSOperationQueue *queue; 
  4   - (IBAction)addOperation:(id)sender; 
  5   - (IBAction)pause:(id)sender; 
  6   - (IBAction)resume:(id)sender; 
  7   @end 
  8   @implementation ViewController 
  9   - (void)viewDidLoad {  
  10     [super viewDidLoad]; 
  11     self.queue = [[NSOperationQueue alloc] init]; 
  12  } 
  13  // 添加operation 
  14  - (IBAction)addOperation:(id)sender { 
  15     // 设置操作的最大并发操作数 
  16     self.queue.maxConcurrentOperationCount = 1; 
   17    for (int i = 0; i < 20; i++) { 
  18        [self.queue addOperationWithBlock:^{ 
  19        // 模拟休眠 
  20         [NSThread sleepForTimeInterval:1.0f]; 
  21         NSLog(@"正在下载  %@ %d", [NSThread currentThread], i); 
  22        }]; 
  23     } 
  24  } 
  25  // 暂停 
  26  - (IBAction)pause:(id)sender { 
  27     // 判断队列中是否有操作 
  28     if (self.queue.operationCount == 0) { 
  29        NSLog(@"没有操作"); 
  30        return; 
  31     } 
  32     // 如果没有被挂起,才需要暂停 
  33     if (!self.queue.isSuspended) { 
  34        NSLog(@"暂停"); 
  35        [self.queue setSuspended:YES]; 
  36     } else{ 
  37        NSLog(@"已经暂停"); 
  38     } 
  39  } 
  40  // 继续 
  41  - (IBAction)resume:(id)sender { 
  42     // 判断队列中是否有操作 
  43     if (self.queue.operationCount == 0) { 
  44        NSLog(@"没有操作"); 
  45        return; 
  46     } 
  47     // 如果没有被挂起,才需要暂停 
  48     if (self.queue.isSuspended) { 
  49        NSLog(@"继续");   
  50        [self.queue setSuspended:NO]; 
  51     } else{ 
  52        NSLog(@"正在执行"); 
  53     } 
  54  } 
  55  @end   

运行程序,程序的运行结果如图1-44所示。

0

图1-44 程序的运行结果

从图1-44中可以看出,当单击“暂停”按钮后,有一个线程还要继续并执行完毕,这是因为当队列执行暂停的时候,这个线程仍在运行,只有其余排队的线程被挂起。