在iOS开发中,多线程技术用到最多的就是GCD和NSOperation,上一篇文章已经对GCD有了全面的了解,这篇文章简单的聊一聊NSOperation。

一、简介

  • 除了,NSThread和GCD实现多线程,配合使用NSOperation和NSOperationQueue也能实现多线程编程

NSOperation和NSOperationQueue实现多线程的具体步骤

  • 1、先将需要执行的操作封装到一个NSOperation的子类对象中
    • 实际上,NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
  • 2、然后将NSOperation对象添加到NSOperationQueue中
  • 3、系统会自动将NSOperationQueue中的NSOperation取出来
  • 4、将取出的NSOperation封装的操作放到一条新线程中执行

二、NSOperation

  • 如上所示:要实现多线程,必须要将执行的操作封装到NSOperation的子类对象中,那么NSOperation的子类有哪些?

1、使用NSOperation子类的方式有3种

  • NSInvocationOperation
  • NSBlockOperation
  • 自定义子类继承NSOperation,实现NSOperation的某些方法
NSInvocationOperation
  • 创建NSInvocationOperation对象
-(id)initWithTarget:(id)target selector:(SEL)selector object:(id)arg;

NSInvocationOperation将操作封装成SEL,只有将NSInvocationOperation添加到NSOperationQueue中,才会异步执行操作。

- (void)test {
// 1.将操作封装到Operation中
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 2.执行封装的操作
// 如果直接执行NSInvocationOperation中的操作, 那么默认会在主线程中执行
//[op1 start];
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//将任务加入到队列 任务会自动开启
[queue addOperation:op1];
}
NSBlockOperation
  • 创建NSBlockOperation 对象
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;

NSBlockOperation将任务封装成一个block,这种方式看起来更方便。当然,你还可以调用-addExecutionBlock:方法,添加另一个任务。

注意点:只要NSBlockOperation封装的操作数 >1,直接调用start方法,就会异步执行操作,但是默认的初始化的block任务是在主线程中执行。

- (void)test {
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行任务");
}];
[blockOperation addExecutionBlock:^{
NSLog(@"开始下一个执行任务");
}];
//如果不将NSBlockOperation添加到队列中,而是直接调用start方法,那么默认初始化时的任务执行在主线程中,而通过addExecutionBlock方法添加的任务都执行在子线程中。
//[blockOperation start];
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//将任务加入到队列 任务会自动开启
[queue addOperation: blockOperation];
}
自定义 NSOperation

首先需要继承NSOperation,然后实现某些方法。
我们有两种方法来实现自定义operation,一种是通过重写main方法,一种是通过重写start方法,前者实现起来更简单,我们不需要管理一些属性状态,例如isExecuting或者isFinished等。当main方法执行结束的时候,这个operation就结束了。另外一种是重写start方法,但是这种方法需要你管理各种属性的状态,虽然麻烦,但是相对灵活。如果你同时实现startmain方法,则优先使用start方法。因为第一种方法比较简单,我们这里以第二种方法来自定义operation

首先,父类提供了一系列的getter方法,用以获知任务的状态。

  • @property (readonly, getter=isReady) BOOL ready
    此属性表明NSOperation是否已经准备好开始执行任务了,如果返回falseNSOperation的之后的方法都不会执行。通常情况下,自定义operation,我们可以利用它来让调用者必须满足一定的条件才能执行任务。

  • @property (readonly, getter=isExecuting) BOOL executing
    是否正在执行。

  • @property (readonly, getter=isFinished) BOOL finished
    任务是否完成

  • @property (readonly, getter=isCancelled) BOOL cancelled
    任务是否取消

  • @property (readonly, getter=isConcurrent) BOOL concurrent
    表示是否异步执行任务

自定义operation如下

MyOperation.h

@interface MyOperation : NSOperation
+ (instancetype)crateOperationWithMoney:(int )money;
@end

MyOperation.m

#import "MyOperation.h"
#import <objc/runtime.h>
@interface MyOperation ()
@property (nonatomic, assign) int money;
@property (nonatomic, assign) BOOL cancelled;
@property (nonatomic, assign) BOOL executing;
@property (nonatomic, assign) BOOL finished;
@property (nonatomic, assign) BOOL concurrent;
@property (nonatomic, assign) BOOL asynchronous;
@end
@implementation MyOperation
@synthesize cancelled;
@synthesize executing;
@synthesize finished;
@synthesize concurrent;
@synthesize asynchronous;
+ (instancetype)crateOperationWithMoney:(int )money {
MyOperation *operation = [[MyOperation alloc] init];
operation.money = money;
return operation;
}
- (BOOL)isReady {
//只有当金额大于等于100的时候才执行任务
return _money >= 100;
}
- (void)start {
//判断任务状态
if (self.isExecuting || self.isCancelled || self.isFinished) {
return;
}
NSLog(@"%@--%@开始花钱",self.name,[NSThread currentThread]);
self.executing = YES;
while (self.money > 0){
self.money--;
NSLog(@"%@花钱了%d",self.name,self.money);
}
//执行完毕
self.finished = YES;
self.executing = NO;
}
- (BOOL)isExecuting {
return self.executing;
}
- (void)cancel {
NSLog(@"%@%@取消",[NSThread currentThread],self.name);
self.finished = YES;
self.cancelled = YES;
self.executing = NO;
}
- (BOOL)isCancelled {
return self.cancelled;
}
- (BOOL)isFinished {
return self.finished;
}
- (BOOL)isConcurrent {
return self.concurrent;
}
- (BOOL)isAsynchronous {
return self.asynchronous;
}
- (void)dealloc {
NSLog(@"%@dealloc",self.name);
}
@end

这仅仅是一个简单的实例,真正的项目中还需要打磨,如果没有特殊的要求,还是使用系统提供的比较好。

添加依赖

目的 -> NSOperation之间可以设置依赖来保证执行顺序
例如:一定要让操作A执行完后,才能执行操作B,可以这么写

[operationB addDependency:operationA];

例如我们通过不同的线程下载图片 当都下载完成以后在合成图片

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
__block UIImage *image1 = nil;
__block UIImage *image2 = nil;
// 1.开启一个线程下载第一张图片
NSOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://cdn.cocimg.com/assets/images/logo.png?v=201510272"];
NSData *data = [NSData dataWithContentsOfURL:url];
// 2.生成下载好的图片
UIImage *image = [UIImage imageWithData:data];
image1 = image;
}];
// 2.开启一个线程下载第二长图片
NSOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com/img/bd_logo1.png"];
NSData *data = [NSData dataWithContentsOfURL:url];
// 2.生成下载好的图片
UIImage *image = [UIImage imageWithData:data];
image2 = image;
}];
// 3.开启一个线程合成图片
NSOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
[image1 drawInRect:CGRectMake(0, 0, 100, 200)];
[image2 drawInRect:CGRectMake(100, 0, 100, 200)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 4.回到主线程更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"回到主线程更新UI");
self.imageView.image = newImage;
}];
}];
// 监听任务是否执行完毕
op1.completionBlock = ^{
NSLog(@"第一张图片下载完毕");
};
op2.completionBlock = ^{
NSLog(@"第二张图片下载完毕");
};
// 添加依赖
// 只要添加了依赖, 那么就会等依赖的任务执行完毕, 才会执行当前任务
// 注意:
// 1.添加依赖, 不能添加循环依赖
// 2.NSOperation可以跨队列添加依赖
[op3 addDependency:op1];
[op3 addDependency:op2];
// 将任务添加到队列中
[queue addOperation:op1];
[queue addOperation:op2];
[queue2 addOperation:op3];
}
@end

三、NSOperationQueue

NSOperation可以调用start方法来执行操作,但是默认是同步执行的。将NSOperation添加到NSOperationQueue中,系统会自动异步调用NSOperationstart方法或者main方法来执行任务。

NSOperationQueue提供了三种添加任务的方法:

  • - (void)addOperation:(NSOperation *)op 将NSOperation对象添加到队列中
  • - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait 将NSOperation对象数组添加到队列中,wait表示是否阻塞当前队列,YES表明以后添加的任务只能在ops中的所有任务执行完成以后才会执行,NO表示不阻塞当前队列
  • - (void)addOperationWithBlock:(void (^)(void))block 将一个block添加到任务中。

这里有几点需要注意的,添加的任务一定不能是正在执行的或者已经完成的,可通过executingfinished来判断。不能重复添加一个operation到同一个队列中,否则会报NSInvalidArgumentException异常。

再来看看NSOperationQueue的属性:

  • operations当前正在执行或者等待执行的任务的集合,如果一个任务执行完毕,会从中删除。
  • operationCount 当前队列中的任务数(正在执行或者等待执行)
  • maxConcurrentOperationCount 最大并发数,默认是-1,即无限大,如果设置为1,那么当前队列即为串行队列,但不能设置成0,如果设置成0,那么当前的所有任务都不能执行。
  • name 队列的名字

当然,NSOperationQueue也提供了取消操作,
- (void)cancelAllOperations,此方法会取消所有未执行的操作。内部实现是分别调用任务的cancel方法。但是已经执行的操作将会继续执行。另外此方法并不会将取消的操作从队列中移除。但是实际却移除了,这是因为NSInvocationOperationNSBlockOperation内部自己管理了finish状态,在cancel方法中会将当前任务的finish设置为YES

- (void)waitUntilAllOperationsAreFinished阻塞当前任务队列。