NSOperation和NSOperationQueue的一些事儿

在面对多线程的时候,大多数会选择NSOperation或者GCD来实现,GCD由于使用起来非常方便,应该是很多开发者的首选,不过你会发现其实很多开源代码都是使用NSOpertaion来执行异步任务,所以这次我们来说说NSOperation跟NSOperationQueue,以及它的强大之处。

NSOPerationQueue

NSOperation可以通过调用start方法同步地执行相应的任务,不过通常NSOprtaion都是配合NSOPerationQueue来使用的,NSOperationQueue可以看作是一种高级的dispatch queue,将NSOperation加入到queue中,queue会自动异步的执行该NSOperation

1
2
3
4
5
//创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

//将NSOperation加入队列之后,queue会自动执行该operation
[queue addOperation:operation];

在gcd编程的时候,我们无法取消block对应的任务,不过NSOPerationQueue之所以称为高级的dispatch queue除了能异步的执行任务之外,还能够:

  • 取消操作
    当NSOperation加入到NSOperationQueue之后,可以通过调用cancel方法取消单个任务,如果想取消所有的任务可以调用cancelAllOperations方法;
1
2
3
4
5
// 取消一个任务 
[operation cancel];

// 取消queue中所有的任务
[queue cancelAllOperations];
  • 设置最大并发数
    我们可以通过设置maxConcurrentOperationCount来设置队列的最大并发数,比如当网络为Wi-Fi时设置为6,3G时设置为2:
1
2
//设置并发数目为2
queue.maxConcurrentOperationCount = 2;

假如maxConcurrentOperationCount的值设为1,可以看作该队列为串行队列,每次只能执行一个任务。不过NSOPerationQueue不是先进先出(FIFO)队列,这点跟dispatch queue有点区别,dispatch queue中的block会按照FIFO的顺序去执行,NSOPerationQueue会根据Operation的状态(是否Ready)以及优先级来确定执行的NSOperation的顺序。

  • 暂停跟继续
    假如想要暂停NSOperation的执行,可以调用queue的setSuspended:方法暂停queue继续调度运行新的任务,不过正在执行的任务无法暂停
1
2
3
4
5
// 暂停队列运行任务
[queue setSuspended:YES];

// 继续
[queue setSuspended:NO];
  • 队列优先级
    iOS8之后引入了QualityOfService的概念,类似于线程的优先级,设置不同的QualityOfService值后系统会分配不同的CPU时间、网络资源和硬盘资源等,因此我们可以通过这个设置队列的优先级,NSQualityOfService定义了几个枚举值:

UserInteractive: 任务跟界面的一些UI相关,比如绘制屏幕内容跟处理点击事件等,处于最高优先级的任务

UserInitiated : 用户一些请求的任务,关系到后面的交互,比如用户点击消息按钮后获取邮件列表的任务

Utility: 处理一些用户并不立即需要结果的任务,比如定期的内容更新之类的任务

Background:后台任务,用户不会察觉到这些任务,比如后台对文件进行索引方便后续搜索,优先级最低;

Default: 默认值,介于UserInitiated跟Utility之间

NSOperaion

NSOperation可以看作是高级的dispatch block(dispatch_block_t),我们可以将一个任务封装成一个NSOperation对象,然后放到NSOperationQueue中去异步执行。NSOperation分为concurrent(并发任务)跟non-concurrent(非并发任务)两种,两者主要是生命周期的管理有些区别,NSOperation是一个抽象类,我们需要继承于它并实现一些方法。(当任务相对简单的时候,可以直接使用NSInvocationOperation或是NSBlockOperation,这两者也都继承自NSOperation)

  • non-concurrent operation
    实现一个non-concurrent operation只需要简单的继承NSOperation并实现main方法即可,NSOperationQueue会调用NSOperation的start方法,然后在start方法中调用main方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface NonConcurrentOperation : NSOperation
@end
@implementation NonConcurrentOperation

-(void)main
{
NSLog(@"main called");

dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
__weak typeof(self) weakSelf = self;
dispatch_after(time, dispatch_get_main_queue(), ^{

NSLog(@"weakSelf:%@",weakSelf);
});
}
-(void)dealloc
{
NSLog(@"dealloc called");
}

@end

non-concurrent operation会同步的运行main方法,不管期间的任务需要运行多长时间,当在执行完main方法后该operation对象会被释放。假如你在main方法中执行了异步的操作时会出现错误,比如上述的代码可以发现main方法运行完后就调用dealloc方法释放了,导致weakSelf的值为空。

1
2
3
NSOperations[1467:91483] main called
NSOperations[1467:91483] dealloc called
NSOperations[1467:91218] weakSelf:(null)
  • concurrent operation
    当你的任务是并发的任务是异步任务时,你需要的是继承NSOperation并至少实现start、isExecuting和isFinished方法,其中isExecuting跟isFinished两个属性值的改变需要做KVO通知。
    concurrent operation的生命周期跟non-concurrent operation不一样,可以在start方法中执行异步的方法,当start方法执行完之后该operation对象不会被释放,直到该operation执行结束,收到isFinished的kvo通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//状态枚举
typedef NS_ENUM(NSInteger, ConcurrentOperationState) {
ConcurrentOperationReadyState = 1,
ConcurrentOperationExecutingState,
ConcurrentOperationFinishedState
};

@interface ConcurrentOperation ()
@property (nonatomic, assign) ConcurrentOperationState state;
@end

@implementation ConcurrentOperation

- (BOOL)isReady {
self.state = ConcurrentOperationReadyState;
return self.state == ConcurrentOperationReadyState;
}
- (BOOL)isExecuting{
return self.state == ConcurrentOperationExecutingState;
}
- (BOOL)isFinished{
return self.state == ConcurrentOperationFinishedState;
}

- (void)start {
__weak typeof(self) weakSelf = self;
dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{

//kvo:结束
[weakSelf willChangeValueForKey:@"isFinished"];
weakSelf.state = ConcurrentOperationFinishedState;
[weakSelf didChangeValueForKey:@"isFinished"];

NSLog(@"finished :%@",weakSelf);
});
NSLog(@"start called");

//kvo:正在执行
[weakSelf willChangeValueForKey:@"isExecuting"];
weakSelf.state = ConcurrentOperationExecutingState;
[weakSelf didChangeValueForKey:@"isExecuting"];
}

-(void)dealloc{
NSLog(@"dealloc called");
}

可以从打印的日志看到,当start方法结束后对象并没有立即被释放,只有发出isFinished的kvo通知后,该operation对象才会被释放

1
2
3
NSOperations[1556:103301] start called
NSOperations[1556:103242] finished :<ConcurrentOperation: 0x79a47bf0>
NSOperations[1556:103242] dealloc called

  • NSOperation状态

NSOperation状态

从上面的图可以看到nsoperation的几种状态,当这几个状态值改变时需要使用KVO通知,其中处于Pending、Ready跟Executing状态的operation是可以被cancel的,而当operation处于finished状态是无法被取消的。当operation成功结束、失败或者被取消了,isFinished的值都会被设置为yes,所以不能仅仅靠isFinished==YES认为operation成功执行。

  • 设置优先级
    我们可以设置operation运行的优先级,优先级的选择主要有:
1
2
3
4
5
6
7
8
9
10
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

//设置优先级
[operation1 setQueuePriority:NSOperationQueuePriorityVeryLow];

当operation被添加到队列之后,NSOperationQueue会浏览所有的operation,优先运行那些处于ready状态且优先级较高的操作。

  • Dependencies
    NSOperation的另一个强大之处就是可以添加依赖,当operation1依赖于operation2的时候,系统可以保证只有当operation2结束的时候,operation1才会运行,而且依赖不局限于一个队列,你可以依赖一个不同队列的NSOperation。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	
@interface NonConcurrentOperation ()
@property(nonatomic,strong)NSNumber *number;
@end

@implementation NonConcurrentOperation

-(id)initWithNumber:(NSNumber *)number{
self = [super init];
if (self) {
self.number = number;
}
return self;
}

-(void)main
{
NSLog(@"main called, %@",self.number);
}
@end

//测试代码
NSOperationQueue *queue1 = [NSOperationQueue new];
NSOperationQueue *queue2 = [NSOperationQueue new];

NonConcurrentOperation *op1 = [[NonConcurrentOperation alloc] initWithNumber:@(1)];
NonConcurrentOperation *op2 = [[NonConcurrentOperation alloc] initWithNumber:@(2)];
NonConcurrentOperation *op3 = [[NonConcurrentOperation alloc] initWithNumber:@(3)];
NonConcurrentOperation *op4 = [[NonConcurrentOperation alloc] initWithNumber:@(4)];

//添加依赖
[op1 addDependency:op2];
[op2 addDependency:op3];

//可以依赖不同队列的operation
[op3 addDependency:op4];

[queue1 addOperation:op1];
[queue1 addOperation:op2];
[queue1 addOperation:op3];
[queue2 addOperation:op4]; //添加到不同队列中

输出结果:

1
2
3
4
NSOperations[2105:179596] main called, 4
NSOperations[2105:179596] main called, 3
NSOperations[2105:179596] main called, 2
NSOperations[2105:179596] main called, 1

添加依赖的时候需要注意不要相互依赖:

1
2
[op1 addDependency:op2];
[op2 addDependency:op1];

如果相互依赖,双方都会等待对方结束导致相互之间都无法执行。


互相依赖.png
  • 抽象化逻辑
    NSOperation可以用来抽象化一些业务逻辑,业务之间有依赖的情况可以通过dependency来简化。比如有的业务场景需要收藏一篇文章,由于收藏文章需要用户的信息,而获取用户的信息又需要去登陆获取,所以我们可以将登陆、获取用户信息以及收藏文章抽象封装封装成3个operation,而其中的逻辑关系通过dependency来实现,降低逻辑复杂度,而且业务之间的关系变得可组装:
1
2
3
4
5
//获取用户信息需要登陆完才能执行
[userInfoOperation addDependency:loginOperation];

//收藏需要获取到用户信息后才能执行
[favorOperation addDependency:userInfoOperation];

业务间的联系.png

不过NSOperation跟NSOperation底层也是基于GCD实现的,它是更高层次的抽象,当你框架设计涉及到这块内容的时候应该优先考虑使用NSOperations,这样框架的结构相对会好些,类似SDwebimgag跟AFNetworking等很多开源框架都在使用NSOperations。不过不可否认gcd代码编写更简便,这边就当抛砖引玉吧~

参考资料

NSOperation
認識NSOperation
Advanced NSOperations