在面对多线程的时候,大多数会选择NSOperation或者GCD来实现,GCD由于使用起来非常方便,应该是很多开发者的首选,不过你会发现其实很多开源代码都是使用NSOpertaion来执行异步任务,所以这次我们来说说NSOperation跟NSOperationQueue,以及它的强大之处。
NSOPerationQueue
NSOperation可以通过调用start方法同步地执行相应的任务,不过通常NSOprtaion都是配合NSOPerationQueue来使用的,NSOperationQueue可以看作是一种高级的dispatch queue,将NSOperation加入到queue中,queue会自动异步的执行该NSOperation
//创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//将NSOperation加入队列之后,queue会自动执行该operation
[queue addOperation:operation];
在gcd编程的时候,我们无法取消block对应的任务,不过NSOPerationQueue之所以称为高级的dispatch queue除了能异步的执行任务之外,还能够:
- 取消操作
当NSOperation加入到NSOperationQueue之后,可以通过调用cancel方法取消单个任务,如果想取消所有的任务可以调用cancelAllOperations方法;
// 取消一个任务
[operation cancel];
// 取消queue中所有的任务
[queue cancelAllOperations];
- 设置最大并发数
我们可以通过设置maxConcurrentOperationCount来设置队列的最大并发数,比如当网络为Wi-Fi时设置为6,3G时设置为2:
//设置并发数目为2
queue.maxConcurrentOperationCount = 2;
假如maxConcurrentOperationCount的值设为1,可以看作该队列为串行队列,每次只能执行一个任务。不过NSOPerationQueue不是先进先出(FIFO)队列,这点跟dispatch queue有点区别,dispatch queue中的block会按照FIFO的顺序去执行,NSOPerationQueue会根据Operation的状态(是否Ready)以及优先级来确定执行的NSOperation的顺序。
- 暂停跟继续
假如想要暂停NSOperation的执行,可以调用queue的setSuspended:方法暂停queue继续调度运行新的任务,不过正在执行的任务无法暂停
// 暂停队列运行任务
[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方法:
@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的值为空。
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通知。
//状态枚举
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对象才会被释放
NSOperations[1556:103301] start called
NSOperations[1556:103242] finished :<ConcurrentOperation: 0x79a47bf0>
NSOperations[1556:103242] dealloc called
- NSOperation状态
从上面的图可以看到nsoperation的几种状态,当这几个状态值改变时需要使用KVO通知,其中处于Pending、Ready跟Executing状态的operation是可以被cancel的,而当operation处于finished状态是无法被取消的。当operation成功结束、失败或者被取消了,isFinished的值都会被设置为yes,所以不能仅仅靠isFinished==YES认为operation成功执行。
- 设置优先级
我们可以设置operation运行的优先级,优先级的选择主要有:
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。
@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]; //添加到不同队列中
输出结果:
NSOperations[2105:179596] main called, 4
NSOperations[2105:179596] main called, 3
NSOperations[2105:179596] main called, 2
NSOperations[2105:179596] main called, 1
添加依赖的时候需要注意不要相互依赖:
[op1 addDependency:op2];
[op2 addDependency:op1];
如果相互依赖,双方都会等待对方结束导致相互之间都无法执行。
- 抽象化逻辑
NSOperation可以用来抽象化一些业务逻辑,业务之间有依赖的情况可以通过dependency来简化。比如有的业务场景需要收藏一篇文章,由于收藏文章需要用户的信息,而获取用户的信息又需要去登陆获取,所以我们可以将登陆、获取用户信息以及收藏文章抽象封装封装成3个operation,而其中的逻辑关系通过dependency来实现,降低逻辑复杂度,而且业务之间的关系变得可组装:
//获取用户信息需要登陆完才能执行
[userInfoOperation addDependency:loginOperation];
//收藏需要获取到用户信息后才能执行
[favorOperation addDependency:userInfoOperation];
不过NSOperation跟NSOperation底层也是基于GCD实现的,它是更高层次的抽象,当你框架设计涉及到这块内容的时候应该优先考虑使用NSOperations,这样框架的结构相对会好些,类似SDwebimgag跟AFNetworking等很多开源框架都在使用NSOperations。不过不可否认gcd代码编写更简便,这边就当抛砖引玉吧~