NSOperation
1、什么是NSOperation?
NSOperation是苹果推荐使用的并发技术,它提供了一些用GCD不是很好实现的功能。
- 基于GCD的面向对象的封装
2、NSOperation的使用
- NSOperation是一个抽象类,不能直接使用,而应该使用它的子类
三种方法:
(1)NSInvocationOperation
(2)NSBlockOperation
(3)自定义继承自NSOperation的子类
联想一下GCD,你会发现,三种子类其实就是GCD中核心概念 --> 任务。
也就是说当你创建了相应的子类对象的实例,就像GCD中任务(Block)一样。
这时候,如果要执行任务,那么你还需要一个队列,将任务添加到队列中才可以。(
NSOperation的使用常常是配合NSOperationQueue来进行的。只要是使用NSOperation的子类创建的实例就能添加到NSOperationQueue操作队列之中,一旦添加到队列,操作就会自动异步执行(注意是异步)。如果没有添加到队列,而是使用start方法,则会在当前线程执行。
下面通过代码来理解一下:
NSInvocationOperation
//1、NSInvocationOperation
NSInvocationOperation *iOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
//在当前线程执行
[iOperation start];
- (void)doSomething{
NSLog(@"%@,%@",[NSThread currentThread],NSStringFromSelector(_cmd));
}
打印结果:
TestApp[962:27659] <NSThread: 0x600000e7a740>{number = 1, name = main},doSomething
NSBlockOperation
//2、NSBlockOperation
NSBlockOperation *bOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@,%@",[NSThread currentThread],NSStringFromSelector(_cmd));
}];
//在当前线程执行
[bOperation start];
打印结果:
TestApp[1019:32858] <NSThread: 0x60000297ad00>{number = 1, name = main},viewDidLoad
NSOperationQueue
上面两个代码都是在当前线程执行,下面看一下加入queue中执行的代码:
//1、NSInvocationOperation
NSInvocationOperation *iOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
//2、NSBlockOperation
NSBlockOperation *bOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@,%@",[NSThread currentThread],NSStringFromSelector(_cmd));
}];
//3、NSOperationQueue
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//将任务放到队列中,只要加入dqueue,任务会自动异步执行
[queue addOperation:iOperation];
[queue addOperation:bOperation];
打印结果:
2019-11-15 14:38:08.609321+0800 TestApp[1043:36587] <NSThread: 0x6000021e2940>{number = 5, name = (null)},doSomething
2019-11-15 14:38:08.609322+0800 TestApp[1043:36588] <NSThread: 0x6000021d4680>{number = 4, name = (null)},viewDidLoad
通过结果可以发现,执行任务的线程已经不是主线程了。NSOperationQueue本质就是GCD里面的并发队列。操作就是GCD里面异步执行的任务
同时,大家可以发现,NSBlockOperation的用法与NSInvocationOperation相同,只是创建的方式不同,它不需要去调用方法,而是直接使用代码块,显得更方便。这也使得NSBlockOperation比NSInvocationOperation更加流行。
事实上NSBlockOperation有更简单的使用方法:
NSOperationQueue *q = [[NSOperationQueue alloc]init];
[q addOperationWithBlock:^{
//任务
NSLog(@"%@",[NSThread currentThread]);
}];
3、线程间通信
当你了解了上面所讲的知识点后,你会发现,GCD里面的核心概念都是一一对应的。
GCD里的任务(Block)对应NSOperation里的子类对象(NSInvocationOpreation和NSBlockOperation)
GCD里的队列(Queue)对应NSOperationQueue
GCD里的异步(dispatch_async) 对应NSOperation里的addOperation方法
所以这里可以明白NSOperation是对GCD的面向对象的封装。
在GCD中,当子线程结束耗时操作(比如下载图片,或者网络请求)后,需要刷新UI时,我们通过
dispatch_async(dispatch_get_main_queue(), ^{
//主线程刷新UI
设置图片,返回数据.......
});
那当我们使用NSOperation怎么获取主队列,从而进一步实现线程间的数据传递呢?
主线程到子线程传对象,前面的例子里面已经有了,不再缀述。下面的例子就是回到主线程更新UI。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSLog(@"耗时操作(下载图片,网络加载)");
//获取主队列,并向主队列添加任务(Block)来进行线程间通信
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//主线程刷新UI
NSLog(@"设置图片,返回数据.......");
}];
}];
4、NSOperationQueue的其他高级操作
NSOperationQueue支持的高级操作有:
(1)设置最大并发数
(2)队列的挂起
(3)队列的取消
(4)添加操作的依赖关系
- 最大并发数:
#pragma mark - 高级操作:线程的挂起
//最大并发数
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//设置最大并发数,(千万注意:别理解错了,最大并发数,不是开辟子线程的数量)
queue.maxConcurrentOperationCount = 2;
for (int i = 0 ; i<100; i++) {
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:3.0];
NSLog(@"%@------%d",[NSThread currentThread],i);
}];
}
打印结果:
2019-11-15 14:58:10.258088+0800 TestApp[1093:45023] <NSThread: 0x6000021f1140>{number = 3, name = (null)}------1
2019-11-15 14:58:10.258139+0800 TestApp[1093:45024] <NSThread: 0x6000021f1300>{number = 4, name = (null)}------0
2019-11-15 14:58:13.259196+0800 TestApp[1093:45025] <NSThread: 0x6000021f2900>{number = 6, name = (null)}------2
2019-11-15 14:58:13.259196+0800 TestApp[1093:45024] <NSThread: 0x6000021f1300>{number = 4, name = (null)}------3
2019-11-15 14:58:16.261188+0800 TestApp[1093:45023] <NSThread: 0x6000021f1140>{number = 3, name = (null)}------4
2019-11-15 14:58:16.261203+0800 TestApp[1093:45024] <NSThread: 0x6000021f1300>{number = 4, name = (null)}------5
2019-11-15 14:58:19.262351+0800 TestApp[1093:45024] <NSThread: 0x6000021f1300>{number = 4, name = (null)}------7
2019-11-15 14:58:19.262386+0800 TestApp[1093:45026] <NSThread: 0x6000021ccc80>{number = 5, name = (null)}------6
2019-11-15 14:58:22.263323+0800 TestApp[1093:45024] <NSThread: 0x6000021f1300>{number = 4, name = (null)}------8
……
……
通过设置最大并发数的个数,和打印结果结合来看,我们发现,任务是两两执行的,而且顺序随机,可以再次说明,NSOperationQueue是并发队列,通过打印的线程number可以看到,参与工作的线程有3,4,5……等,所以可以明白,最大设置最大并发数,并不是设置开启子线程的数量。
- 队列挂起:
同样的我们利用结合上面最大并发数的代码来异步执行两两并发来完成任务,同时,我们新增一个button,来控制我们的队列挂起和执行:
#pragma mark - 高级操作:线程的挂起
//暂停继续(对队列的暂停和继续),挂起的是队列,不会影响已经在执行的操作
- (IBAction)pause:(UIButton *)sender {
//判断操作的数量,当前队列里面是不是有操作?
if (self.opQueue.operationCount == 0) {
NSLog(@"当前队列没有操作");
return;
}
self.opQueue.suspended = !self.opQueue.isSuspended;
if (self.opQueue.suspended) {
NSLog(@"暂停");
}else{
NSLog(@"继续");
}
}
- 队列取消:
同样新增一个button,来实现队列取消:
#pragma mark - 高级操作:取消队列里的所有操作
- (IBAction)cancelAll:(UIButton *)sender {
//只能取消所有队列的里面的操作,正在执行的无法取消
//取消操作并不会影响队列的挂起状态
[self.opQueue cancelAllOperations];
NSLog(@"取消队列里所有的操作");
//取消队列的挂起状态
//(只要是取消了队列的操作,我们就把队列处于不挂起状态,以便于后续的开始)
self.opQueue.suspended = NO;
}
如果你想取消单独的一个任务,那你需要将任务保存起来,当需要取消任务时,判断任务是否执行中,是否finish,然后调用cancel方法即可。
- 添加操作的依赖关系:
啥也不说了,通过一个实际业务来说明再好不过
业务需求:
(1)下载一个小说压缩包
(2)解压缩,删除压缩包
(3)更新UI
//设置依赖关系
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始下载小说压缩包...%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:5];
NSLog(@"小说压缩包已下载完成");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始解压...%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
NSLog(@"解压完成");
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"刷新UI..%@",[NSThread currentThread]);
}];
//添加依赖
//指定任务之间的依赖关系 --依赖关系可以跨队列(可以再子线程下载,在主线程更新UI)
[op2 addDependency:op1];
[op3 addDependency:op2];
//waitUntilFinished 类似GCD中的调度组的通知
//NO不等待,直接执行输出come here
//YES等待任务执行完再执行输出come here
[queue addOperations:@[op1,op2] waitUntilFinished:YES];
[[NSOperationQueue mainQueue] addOperation:op3];
NSLog(@"come here");
打印结果:
2019-11-15 15:21:42.431157+0800 TestApp[1130:54874] 开始下载小说压缩包...<NSThread: 0x600000859640>{number = 7, name = (null)}
2019-11-15 15:21:47.434093+0800 TestApp[1130:54874] 小说压缩包已下载完成
2019-11-15 15:21:47.434598+0800 TestApp[1130:54878] 开始解压...<NSThread: 0x600000850ec0>{number = 6, name = (null)}
2019-11-15 15:21:49.435576+0800 TestApp[1130:54878] 解压完成
2019-11-15 15:21:49.449700+0800 TestApp[1130:54719] 刷新UI..<NSThread: 0x60000083ed40>{number = 1, name = main}
5、NSOperation 和 GCD 的比较?
GCD : 出现版本iOS4.0
OP: 出现版本iOS2.0(2.0时不好用,后期进行了改造)
使用方式:
GCD: 将任务加入到队列(串行,并发),指定任务执行方式(同步,异步)
OP: 将任务加入的并发队列,指定任务以异步的方式执行
获取主队列:
GCD: dispatch_get_main_queu()
OP: [NSOperationQueue mainQueue]
高级操作:
GCD: 延迟执行,调度组
OP: 最大并发数,队列暂停,重启,取消任务(取消所有任务),设置依赖关系,
所以,GCD是比较底层的封装,我们知道较低层的代码一般性能都是比较高的,相对于NSOperation。所以追求性能,而功能够用的话就可以考虑使用GCD。如果异步操作的过程需要更多的用户交互和被UI显示出来,NSOperationQueue会是一个好选择。如果任务之间没有什么依赖关系,而是需要更高的并发能力,GCD则更有优势。