可以将NSOperation理解为任务操作,可以将NSOperationQueue理解为操作队列。NSOperation是基于Objective-C封装的一套管理与执行线程操作的类。这个类是抽象的类,通常情况下,我们会使用NSInvocationOperation和NSBlockOperation这两个子类进行多线程开发,当然也可以写继承于NSOperation的类,自己封装我们自己的操作类;
1.关于NSOperation基类的解析
NSOperation类是NSInvocationOperation和NSBlockOperation这两个类的父类,
1.1 方法
//开启任务
- (void)start;
//取消任务
- (void)cancel;
//这个方法留给子类重写,子类可以在这个方法中实现具体的操作执行行为:
- (void)main;
//阻塞当前线程,直到操作完成任务
- (void)waitUntilFinished;
@property (readonly, getter=isCancelled) BOOL cancelled;
1.2 属性
//当前操作任务是否取消执行
@property (readonly, getter=isCancelled) BOOL cancelled;
//当前操作任务是否正在执行
@property (readonly, getter=isExecuting) BOOL executing;
//当前操作任务是否执行完成
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isConcurrent) BOOL concurrent;
//当前操作任务是否在异步线程中
@property (readonly, getter=isAsynchronous) BOOL asynchronous;
//当前操作任务是否已准备好
@property (readonly, getter=isReady) BOOL ready;
//设置在操作队列中的优先级
@property NSOperationQueuePriority queuePriority;
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L, //优先级最低
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8 //优先级最高
};
//设置线程优先级
@property double threadPriority;
//设置当前操作的名称
@property (nullable, copy) NSString *name;
2.NSBlockOperation类的应用
其可以异步地执行多个Block代码块,当所有的Block都执行完成,这个操作才算完成
2.1 方法和属性
//类方法进行初始化
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
//当初始化完成后,可以使用下面的方法来追加任务Block
- (void)addExecutionBlock:(void (^)(void))block;
//下面属性可以获取NSBlockOperation对象中所追加的所有任务
@property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;
2.2示例代码如下:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i<5; i++) {
NSLog(@"%@===%d",[NSThread currentThread],i);
}
}];
[operation addExecutionBlock:^{
for (int i = 0; i<5; i++) {
NSLog(@"%@===%d",[NSThread currentThread],i);
}
}];
[operation start];
2.3运行结果:
2020-10-26 19:48:53.647881+0800 FMDBActions[37958:1878061] <NSThread: 0x600001ce8b80>{number = 5, name = (null)}===0
2020-10-26 19:48:53.647883+0800 FMDBActions[37958:1877776] <NSThread: 0x600001c80dc0>{number = 1, name = main}===0
2020-10-26 19:48:53.647993+0800 FMDBActions[37958:1878061] <NSThread: 0x600001ce8b80>{number = 5, name = (null)}===1
2020-10-26 19:48:53.648016+0800 FMDBActions[37958:1877776] <NSThread: 0x600001c80dc0>{number = 1, name = main}===1
2020-10-26 19:48:53.648091+0800 FMDBActions[37958:1878061] <NSThread: 0x600001ce8b80>{number = 5, name = (null)}===2
2020-10-26 19:48:53.648124+0800 FMDBActions[37958:1877776] <NSThread: 0x600001c80dc0>{number = 1, name = main}===2
2020-10-26 19:48:53.648192+0800 FMDBActions[37958:1878061] <NSThread: 0x600001ce8b80>{number = 5, name = (null)}===3
2020-10-26 19:48:53.648230+0800 FMDBActions[37958:1877776] <NSThread: 0x600001c80dc0>{number = 1, name = main}===3
2020-10-26 19:48:53.648296+0800 FMDBActions[37958:1878061] <NSThread: 0x600001ce8b80>{number = 5, name = (null)}===4
2020-10-26 19:48:53.648330+0800 FMDBActions[37958:1877776] <NSThread: 0x600001c80dc0>{number = 1, name = main}===4
可以看到两个for循环操作是在不同的线程中执行的,并且是异步执行的, NSBlockOperation其会自动根据加入其中的任务Block来分配线程,使之异步执行。
3.NSInvocationOperation类的应用
NSInvocationOperation和NSBlockOperation不同的是:这个类所执行的操作是在当前线程中同步执行的。
3.1 方法
//通过选择器来初始化对象
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
//通过NSInvocation来初始化
- (instancetype)initWithInvocation:(NSInvocation *)inv;
3.2 实例代码
- (void)viewDidLoad {
[super viewDidLoad];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(InvocationOperation) object:nil];
[operation start];
for (int i = 0; i<5; i++) {
NSLog(@"%@===%d",[NSThread currentThread],i);
}
}
- (void)InvocationOperation{
for (int i = 0; i<5; i++) {
NSLog(@"%@===%d",[NSThread currentThread],i);
}
}
运行结果
020-10-26 20:05:53.619785+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===5
2020-10-26 20:05:53.619956+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===6
2020-10-26 20:05:53.620071+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===7
2020-10-26 20:05:53.620185+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===8
2020-10-26 20:05:53.620293+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===9
2020-10-26 20:05:53.620407+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===0
2020-10-26 20:05:53.620511+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===1
2020-10-26 20:05:53.620627+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===2
2020-10-26 20:05:53.621038+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===3
2020-10-26 20:05:53.621358+0800 FMDBActions[38045:1888658] <NSThread: 0x6000023e8f80>{number = 1, name = main}===4
打印的信息可知NSInvocationOperation中的任务也是在主线程中执行的
4.NSOperationQueue
4.1主队列
通过[NSOperationQueue mainQueue]获取主队列,一般添加到主队列中的任务是在主线程中执行,但是通过NSBlockOperation添加的任务数如果大于1,那么是会开启新的线程去执行任务的。
- (void)viewDidLoad {
[super viewDidLoad];
NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2-->%@",[NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"3-->%@",[NSThread currentThread]);
}];
[queue addOperation:invocationOperation];
[queue addOperation:blockOperation];
}
- (void)task{
NSLog(@"1-->%@",[NSThread currentThread]);
}
运行结果:
2020-10-26 20:20:41.985932+0800 FMDBActions[38109:1897007] 1--><NSThread: 0x600003bbcdc0>{number = 1, name = main}
2020-10-26 20:20:41.987755+0800 FMDBActions[38109:1897087] 3--><NSThread: 0x600003b86dc0>{number = 6, name = (null)}
2020-10-26 20:20:41.987759+0800 FMDBActions[38109:1897007] 2--><NSThread: 0x600003bbcdc0>{number = 1, name = main}
4.2自定义队列
通过[[NSOperationQueue alloc] init]创建自定义队列,添加到自定义队列中的任务会自动开启子线程去执行(子线程的创建由系统控制,添加的多个任务可能在同一个子线程中执行也可能在不同子线程中执行)。自定义队列和前面主队列的使用方法一样;
4.3向队列中添加任务
//添加单个任务(操作)。
- (void)addOperation:(NSOperation *)op;
//添加多个任务,wait为YES的话会阻塞线程,所有添加的任务都执行完后才会继续执行后面的代码,为NO的话会先执行后面的代码再执行添加到队列的任务。
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait);
//以block的形式添加任务。
- (void)addOperationWithBlock:(void (^)(void))block);
4.4 maxConcurrentOperationCount 控制串行、并发执行
可以设置队列的maxConcurrentOperationCount属性来设置最大并发数(这里注意最大并发数是指能并发执行的最大操作数,这并不等同于最大开启的线程数,可以通过下面代码进行验证)。其默认值是-1,表示最大并发数没有限制(当然也不会超过系统设定的默认最大值),如果设置为1的话就表示一次只能调度执行一个任务,也就是串行执行任务,不过这里的串行和GCD的串行有点不一样,GCD的串行是先添加的就先调度执行(也就是遵循FIFO原则),但这里的串行执行顺序和任务的优先级、依赖等因素有关,优先级高的任务后添加进队列也可能会先调度执行。
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];
}
运行结果:
2020-10-26 20:43:14.193715+0800 FMDBActions[38212:1911384] 1--><NSThread: 0x600001555c80>{number = 6, name = (null)}
2020-10-26 20:43:14.193722+0800 FMDBActions[38212:1911387] 0--><NSThread: 0x600001518740>{number = 3, name = (null)}
2020-10-26 20:43:14.397469+0800 FMDBActions[38212:1911384] 3--><NSThread: 0x600001555c80>{number = 6, name = (null)}
2020-10-26 20:43:14.397489+0800 FMDBActions[38212:1911385] 2--><NSThread: 0x600001500040>{number = 4, name = (null)}
2020-10-26 20:43:14.601931+0800 FMDBActions[38212:1911387] 4--><NSThread: 0x600001518740>{number = 3, name = (null)}
当queue.maxConcurrentOperationCount = 1;时运行结果
2020-10-26 20:44:43.523855+0800 FMDBActions[38229:1912673] 0--><NSThread: 0x6000017c2000>{number = 7, name = (null)}
2020-10-26 20:44:43.727440+0800 FMDBActions[38229:1912677] 1--><NSThread: 0x6000017b4040>{number = 3, name = (null)}
2020-10-26 20:44:43.930589+0800 FMDBActions[38229:1912673] 2--><NSThread: 0x6000017c2000>{number = 7, name = (null)}
2020-10-26 20:44:44.134894+0800 FMDBActions[38229:1912673] 3--><NSThread: 0x6000017c2000>{number = 7, name = (null)}
2020-10-26 20:44:44.339484+0800 FMDBActions[38229:1912673] 4--><NSThread: 0x6000017c2000>{number = 7, name = (null)}
4.5队列的挂起与取消
当队列中还有未调度的任务时,将队列的suspended设置为YES表示暂停调度,再次设置为NO表示继续调度任务,cancelAllOperations可以取消所有为调度的任务。比如一个页面有很多张图片要下载,当我们退出这个页面时,还未下载的图片就不需要下载了,我们就可以将已经添加到队列中的还未执行的下载任务给取消掉。如果想要知道队列中的任务是否执行完了,可以通过KVO监听队列的operationCount属性值,为0时表示全部执行完了。 这里要注意,不管是挂起还是取消,都是对队列进行操作,只对队列中还未执行(也就是正在等待调度)的任务有效,如果是已经调度出来正在运行的任务是无法挂起和取消的
4.6线程通信
开发中通常将耗时操作(比如网络请求、下载等)放在子线程中执行,耗时操作完成时需要回到主线程刷新UI,这就需要用到线程通信
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始子线程任务--%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:1]; // 模拟耗时操作
NSLog(@"结束子线程任务--%@",[NSThread currentThread]);
// 回到主线程刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"回到主线程刷新UI--%@",[NSThread currentThread]);
}];
}];
[queue addOperation:blockOperation];
运行结果:
2020-10-26 20:48:47.904284+0800 FMDBActions[38258:1915303] 开始子线程任务--<NSThread: 0x600003055900>{number = 4, name = (null)}
2020-10-26 20:48:48.908722+0800 FMDBActions[38258:1915303] 结束子线程任务--<NSThread: 0x600003055900>{number = 4, name = (null)}
2020-10-26 20:48:48.909149+0800 FMDBActions[38258:1915226] 回到主线程刷新UI--<NSThread: 0x600003018f40>{number = 1, name = main}
4.7线程之间的依赖
项目中经常会遇到一个任务依赖另一个任务的需求,比如一个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
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任务1");
}];
//优先级高
blockOperation1.queuePriority = NSOperationQueuePriorityVeryHigh;
NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任务2");
}];
//默认优先级
blockOperation2.queuePriority = NSOperationQueuePriorityNormal;
NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任务3");
}];
//低的优先级
blockOperation3.queuePriority = NSOperationQueuePriorityVeryLow;
// 添加依赖
[blockOperation1 addDependency:blockOperation2]; // bo1依赖bo2,也就是bo2执行完了才会执行bo1
[blockOperation2 addDependency:blockOperation3]; // bo2依赖bo3,也就是bo3执行完了才会执行bo2
[queue addOperations:@[blockOperation1,blockOperation2,blockOperation3] waitUntilFinished:NO];
// 回到主线程刷新UI
NSBlockOperation *blockOperation4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"回到主线程刷新UI-->%@",[NSThread currentThread]);
}];
// 有依赖关系的2个任务可以不在一个队列中
[blockOperation4 addDependency:blockOperation1];
[[NSOperationQueue mainQueue] addOperation:blockOperation4];
不添加依赖运行结果:可以看到其优先级只是相对的,不是一定先执行这个任务的
2020-10-26 21:03:26.562636+0800 FMDBActions[38352:1926071] 任务3
2020-10-26 21:03:26.562638+0800 FMDBActions[38352:1926066] 任务1
2020-10-26 21:03:26.562636+0800 FMDBActions[38352:1926069] 任务2
2020-10-26 21:03:26.565778+0800 FMDBActions[38352:1925957] 回到主线程刷新UI--><NSThread: 0x600000a34f80>{number = 1, name = main}
添加依赖的运行结果:
2020-10-26 20:55:12.836328+0800 FMDBActions[38288:1919265] 任务3
2020-10-26 20:55:13.840696+0800 FMDBActions[38288:1919273] 任务2
2020-10-26 20:55:14.842495+0800 FMDBActions[38288:1919272] 任务1
2020-10-26 20:55:14.843068+0800 FMDBActions[38288:1919203] 回到主线程刷新UI--><NSThread: 0x600001f9c1c0>{number = 1, name = main}