1.NSOperation简介
NSOperation
是基于GCD
更高一层的封装,是面向对象的。相比GCD
,NSOperation
的使用更加简单,并且提供了一些用GCD不是很好实现的功能(比如设置最大并发数、队列的暂停和继续、取消任务、指定任务的依赖关系等)。
NSOperation
是一个抽象类(c++中用virtual
修饰的函数为虚函数
,虚函数是有方法实现的,虚函数的作用就是允许其在子类中可以被重写,所以OC的方法其实都是虚函数。虚函数后面加上=0
就是纯虚函数
,比如virtual void add(int a,int b) = 0;
就是纯虚函数,纯虚函数是没有函数实现的,只能在子类中去实现。一个类只要有纯虚函数那它就是一个抽象类,抽象类不能用来实例化对象,只能作为基类用来继承并在子类中重写虚函数后才能使用。其实OC中是没有抽象类的,但可以结合OC的协议来实现抽象类。
),也就是说它并不能直接使用,而是应该使用它的子类。使用它的子类的方法有三种,使用苹果为我们提供的两个子类 NSInvocationOperation,NSBlockOperation和自定义继承自NSOperation的子类。
2.NSInvocationOperation的使用
NSInvocationOperation
直接使用时默认同步执行,也就是不会开启新的线程去执行任务,而是在当前线程执行(如下所示,test
函数是在哪个线程执行,那么task
也就是在哪个线程执行)。
- (void)test{
NSInvocationOperation *io = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
[io start];
}
- (void)task{
NSLog(@"-->%@",[NSThread currentThread]);
}
// 打印结果
2019-11-27 10:19:51.304818+0800 myTest[10234:4208794] --><NSThread: 0x1007035d0>{number = 1, name = main}
3.NSBlockOperation的使用
如果只有一个任务(或者叫操作),也就是通过blockOperationWithBlock
添加的执行任务(或者通过init方法创建NSBlockOperation
对象然后通过blockOperationWithBlock
添加一个任务),那不会开启新线程,而是在当前线程执行任务。
- (void)test{
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1-->%@",[NSThread currentThread]);
}];
[bo start];
}
// 打印结果
2019-11-27 11:02:47.763536+0800 myTest[10825:4360047] 1--><NSThread: 0x10050b240>{number = 1, name = main}
如果通过addExecutionBlock
额外添加了多个执行任务,那么会开启新线程去执行任务,此时blockOperationWithBlock
添加的任务也不一定是在当前线程中执行。所有任务完成后执行completionBlock
中的代码,注意completionBlock
要写在start
方法的前面。
- (void)test{
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1-->%@",[NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"2-->%@",[NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"3-->%@",[NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"4-->%@",[NSThread currentThread]);
}];
bo.completionBlock = ^{
NSLog(@"完成");
};
[bo start];
}
// 打印结果
2019-11-27 11:09:45.522125+0800 myTest[10884:4381034] 4--><NSThread: 0x1018071f0>{number = 4, name = (null)}
2019-11-27 11:09:45.522129+0800 myTest[10884:4381032] 1--><NSThread: 0x101800050>{number = 2, name = (null)}
2019-11-27 11:09:45.522114+0800 myTest[10884:4381033] 3--><NSThread: 0x1018000b0>{number = 3, name = (null)}
2019-11-27 11:09:45.522115+0800 myTest[10884:4380754] 2--><NSThread: 0x10050b240>{number = 1, name = main}
2019-11-27 11:09:45.522120+0800 myTest[10884:4380754] 完成
4.自定义继承自 NSOperation 的子类
我们还可以自定义继承自 NSOperation 的子类,重写 main
方法 ,将要执行的任务放在main方法中,当调用start
方法时会自动执行main
方法中的代码。
// 自定义子类的.h文件
#import <UIKit/UIKit.h>
@interface QJOperation : NSOperation
@end
// 自定义子类的.m文件
#import "QJOperation.h"
@implementation QJOperation
- (void)main
{
// 要执行的任务
for (NSInteger i = 0; i < 3; i++) {
[NSThread sleepForTimeInterval:0.5];
NSLog(@"-->%@",[NSThread currentThread]);
}
}
@end
// 使用 QJOperation
- (void)useCustomOperation {
QJOperation *co = [[QJOperation alloc] init];
[co start];
}
5.NSOperationQueue
5.1 主队列
通过[NSOperationQueue mainQueue]
获取主队列,一般添加到主队列中的任务是在主线程中执行,但是通过NSBlockOperation
添加的任务数如果大于1,那么是会开启新的线程去执行任务的。
NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSInvocationOperation *io = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2-->%@",[NSThread currentThread]);
}];
[bo addExecutionBlock:^{
NSLog(@"3-->%@",[NSThread currentThread]);
}];
[queue addOperation:io];
[queue addOperation:bo];
- (void)task{
NSLog(@"1-->%@",[NSThread currentThread]);
}
// 打印结果
2019-11-27 16:00:44.342231+0800 AppTest[9485:3654469] 1--><NSThread: 0x282b4a000>{number = 1, name = main}
2019-11-27 16:00:44.344664+0800 AppTest[9485:3654502] 3--><NSThread: 0x282bcc340>{number = 5, name = (null)}
2019-11-27 16:00:44.344674+0800 AppTest[9485:3654469] 2--><NSThread: 0x282b4a000>{number = 1, name = main}
5.2 自定义队列
通过[[NSOperationQueue alloc] init]
创建自定义队列,添加到自定义队列中的任务会自动开启子线程去执行(子线程的创建由系统控制,添加的多个任务可能在同一个子线程中执行也可能在不同子线程中执行)。自定义队列和前面主队列的使用方法一样。
5.3 向队列中添加任务
- (void)addOperation:(NSOperation *)op;
添加单个任务(操作)。
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait);
添加多个任务,wait
为YES的话会阻塞线程,所有添加的任务都执行完后才会继续执行后面的代码,为NO的话会先执行后面的代码再执行添加到队列的任务。
- (void)addOperationWithBlock:(void (^)(void))block);
以block的形式添加任务。
5.4 maxConcurrentOperationCount 控制串行、并发执行
可以设置队列的maxConcurrentOperationCount
属性来设置最大并发数(这里注意最大并发数是指能并发执行的最大操作数,这并不等同于最大开启的线程数,可以通过下面代码进行验证)。其默认值是-1,表示最大并发数没有限制(当然也不会超过系统设定的默认最大值),如果设置为1的话就表示一次只能调度执行一个任务,也就是串行执行任务,不过这里的串行和GCD的串行有点不一样,GCD的串行是先添加的就先调度执行(也就是遵循FIFO原则),但这里的串行执行顺序和任务的优先级、依赖等因素有关,优先级高的任务后添加进队列也可能会先调度执行。
// 设置最大并发数为2,但打印显示线程数并不一定是2
- (void)test{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;
for (NSInteger i = 0; i < 5; i++) {
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.2];
NSLog(@"%ld-->%@",i,[NSThread currentThread]);
}];
[queue addOperation:bo];
}
}
// 打印结果
2019-11-27 17:08:59.642005+0800 AppTest[9693:3678978] 0--><NSThread: 0x28132c680>{number = 5, name = (null)}
2019-11-27 17:08:59.642002+0800 AppTest[9693:3678982] 1--><NSThread: 0x28132c800>{number = 4, name = (null)}
2019-11-27 17:08:59.842901+0800 AppTest[9693:3678979] 2--><NSThread: 0x28131c840>{number = 6, name = (null)}
2019-11-27 17:08:59.842904+0800 AppTest[9693:3678978] 3--><NSThread: 0x28132c680>{number = 5, name = (null)}
2019-11-27 17:09:00.048630+0800 AppTest[9693:3678979] 4--><NSThread: 0x28131c840>{number = 6, name = (null)}
5.5 队列的挂起与取消
但队列中还有未调度的任务时,将队列的suspended
设置为YES表示暂停调度,再次设置为NO表示继续调度任务,cancelAllOperations
可以取消所有为调度的任务。比如一个页面有很多张图片要下载,当我们退出这个页面时,还未下载的图片就不需要下载了,我们就可以将已经添加到队列中的还未执行的下载任务给取消掉。如果想要知道队列中的任务是否执行完了,可以通过KVO
监听队列的operationCount
属性值,为0时表示全部执行完了。
这里要注意,不管是挂起还是取消,都是对队列进行操作,只对队列中还未执行(也就是正在等待调度)的任务有效,如果是已经调度出来正在运行的任务是无法挂起和取消的。
// 暂停/继续
- (void)suspendOperations{
self.queue.suspended = !self.queue.isSuspended;
if (self.queue.suspended) {
NSLog(@"暂停");
}else{
NSLog(@"继续");
}
}
// 取消所有操作
- (void)cancelAllOperations{
[self.queue cancelAllOperations];
NSLog(@"取消所有操作");
}
5.6 线程通信
开发中通常将耗时操作(比如网络请求、下载等)放在子线程中执行,耗时操作完成时需要回到主线程刷新UI,这就需要用到线程通信。
- (void)test{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始子线程任务--%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:1]; // 模拟耗时操作
NSLog(@"结束子线程任务--%@",[NSThread currentThread]);
// 回到主线程刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"回到主线程刷新UI--%@",[NSThread currentThread]);
}];
}];
[queue addOperation:bo];
}
// 打印结果
2019-11-28 09:43:24.313447+0800 AppTest[10330:3871233] 开始子线程任务--<NSThread: 0x28179cec0>{number = 4, name = (null)}
2019-11-28 09:43:25.318741+0800 AppTest[10330:3871233] 结束子线程任务--<NSThread: 0x28179cec0>{number = 4, name = (null)}
2019-11-28 09:43:25.319329+0800 AppTest[10330:3871202] 回到主线程刷新UI--<NSThread: 0x2817fe000>{number = 1, name = main}
6. 操作的优先级与依赖
6.1 优先级
一个任务(操作)的默认优先级为NSOperationQueuePriorityNormal
,优先级高的任务只是说被调度的几率更大,并不能保证一定先执行。即便是最大并发数为1时,后添加进队列的任务如果优先级高也可能会先调度执行(注意先调度并不代表先执行完,这和任务要执行的时长也有关系)。另外只有在同一个队列中的任务设置不同优先级才会起作用,也就是说不同的队列中的任务的优先级是相互独立的。
// 操作的优先级
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0, // 默认优先级
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
6.2 操作的依赖关系
项目中经常会遇到一个任务依赖另一个任务的需求,比如一个APP需要先登录成功后才能进行其他网络请求操作,也就是说其他网络请求操作是依赖登录操作的。这里就可以通过添加操作的依赖关系来实现这样的需求,但是添加依赖时要注意不要出现相互依赖的情况,比如a依赖b,b依赖c,c又依赖a就会出问题。另外有依赖关系的2个任务可以不在一个队列中
。
添加依赖后就是按照依赖的顺序来执行,此时任务的优先级就不起作用了。当一个任务添加依赖后,在它说依赖的任务执行完之前,它的状态是未就绪的状态(也就是isReady
属性为NO),所有依赖都执行完后isaReady
才变为YES,此时这个任务才能被调度执行。
添加多个依赖也是可以的,比如如果bo1任务是下载图片a,bo2任务是下载图片b,bo3的任务是拼接图片a和b,那么bo1和bo2是没有依赖关系的,但是bo3要同时依赖bo1和bo2([bo3 addDependency:bo1];[bo3 addDependency:bo2];),那么bo1和bo2都执行完成后才执行bo3。
- (void)test{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任务1");
}];
bo1.queuePriority = NSOperationQueuePriorityVeryHigh;
NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任务2");
}];
bo2.queuePriority = NSOperationQueuePriorityNormal;
NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任务3");
}];
bo3.queuePriority = NSOperationQueuePriorityVeryLow;
// 添加依赖
[bo1 addDependency:bo2]; // bo1依赖bo2,也就是bo2执行完了才会执行bo1
[bo2 addDependency:bo3]; // bo2依赖bo3,也就是bo3执行完了才会执行bo2
[queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:NO];
// 回到主线程刷新UI
NSBlockOperation *bo4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"回到主线程刷新UI-->%@",[NSThread currentThread]);
}];
// 有依赖关系的2个任务可以不在一个队列中
[bo4 addDependency:bo1];
[[NSOperationQueue mainQueue] addOperation:bo4];
}
// 打印结果(执行顺序始终是3,2,1)
2019-11-27 17:58:23.749955+0800 AppTest[9748:3692966] 任务3
2019-11-27 17:58:24.755548+0800 AppTest[9748:3692969] 任务2
2019-11-27 17:58:25.761256+0800 AppTest[9748:3692968] 任务1
2019-11-27 17:58:25.870941+0800 AppTest[10277:3859398] 回到主线程刷新UI--><NSThread: 0x28291df80>{number = 1, name = main}
网友评论