前序:按顺序阅读更好
一 简介
NSOperation是对GCD的包装,GCD只支持FIFO的队列,而NSOpration可以设置最大并发数、设置优先级、添加依赖关系等调整执行顺序,NSOpration甚至可以跨队列设置依赖关系
NSOperatio有2个核心概念:NSOperation(操作)和NSOperationQueue(队列). NSOperation是个抽象类,依赖于子类NSInvocationOperation、NSBlockOperation去实现,另外还可以自定义NSOperation.
二 基本使用
1. NSOperationQueue
NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
2. NSOperation
2.1 NSInvocationOperation
① 基本使用
- (void)invocationOperation {
// 处理事务
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(handleInvocation:) object:@"a"];
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 操作加入队列
[queue addOperation:op];
}
② 直接处理事务,不添加隐性队列
//在主线程单独使用子类 NSInvocationOperation
- (void)invocationOperation {
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation) object:@"a"];
[op start];
}
-(void)handleInvocation{
[NSThread sleepForTimeInterval:2];// 模拟耗时操作
NSLog(@"%d", [NSThread currentThread].isMainThread); // 打印当前线程
}
//在子线程使用子类 NSInvocationOperation
- (void)invocationOperation {
[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];
}
-(void)handleInvocation{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"%d", [NSThread currentThread].isMainThread); // 打印当前线程
}
- 如果不使用队列NSOperationQueue,单独使用子类NSOperation(NSInvocationOperation/NSBlockOperation/CustomOperation)执行
一个操作的情况下, 操作是在当前线程执行的,并没有开启新线程(如果在主线程中执行,任务就在主线程;如果其他线程中执行,任务就在其他线程)。 - 如果不使用队列NSOperationQueue,单独使用子类NSBlockOperation执行
多个操作的情况下,如果在主线程中执行,只有第一个任务在主线程,其他的需要开启线程,开启的线程数是由系统来决定的;如果在子线程中执行,系统会根据操作的个数来决定是否开启新线程。 - 如果结合队列NSOperationQueue使用NSOperation(NSInvocationOperation/NSBlockOperation),会自动开启新线程 ③ 错误使用
- (void)invocationOperation {
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"a"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];
[op start];
}
--------------------错误日志:-------------------
something is trying to start the receiver simultaneously from more than one thread'
--------------------错误日志:-------------------
上述代码之所以会崩溃,是因为线程生命周期:
[queue addOperation:op]已经将处理事务的操作任务加入到队列中,并让线程运行
[op start]将已经运行的线程再次运行会造成线程混乱
2.2 NSBlockOperation
NSInvocationOperation和NSBlockOperation两者的区别在于: 前者类似target形式;后者类似block形式——函数式编程,业务逻辑代码可读性更高。
执行多个任务有两种写法:
- 用
addExecutionBlock方法往一个NSBlockOperation中追加多个任务,然后添加到一个NSOperationQueue中。- 额外操作(包括blockOperationWithBlock中的操作)可以在不同的线程中同时(并发)执行。completionBlock方法会在所有异步操作执行完后才执行,也就是只有当所有相关的操作已经完成执行时,才视为完成。
- 定义多个
NSBlockOperation,添加到一个NSOperationQueue中
2.2.1 第一种写法
- (void)blockOperation {
// 初始化添加事务
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1————%@",[NSThread currentThread]);
}];
// 添加事务1
[bo addExecutionBlock:^{
NSLog(@"任务2————%@",[NSThread currentThread]);
}];
// 添加事务2
[bo addExecutionBlock:^{
NSLog(@"任务3————%@",[NSThread currentThread]);
}];
// 添加事务3
[bo addExecutionBlock:^{
NSLog(@"任务4————%@",[NSThread currentThread]);
}];
// 添加事务4
[bo addExecutionBlock:^{
NSLog(@"任务5————%@",[NSThread currentThread]);
}];
// 回调监听
bo.completionBlock = ^{
NSLog(@"completionBlock执行,任务全部完成");
};
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:bo];
NSLog(@"事务添加进了NSOperationQueue");
}
--------------------输出结果:-------------------
事务添加进了NSOperationQueue
任务1————<NSThread: 0x280be7d00>{number = 3, name = (null)}
任务4————<NSThread: 0x280be7d00>{number = 3, name = (null)}
任务3————<NSThread: 0x280bc0340>{number = 6, name = (null)}
任务2————<NSThread: 0x280bd8600>{number = 5, name = (null)}
任务5————<NSThread: 0x280be7d00>{number = 3, name = (null)}
completionBlock执行,任务全部完成
--------------------输出结果:-------------------
NSOperationQueue是异步执行的,所以任务12345的完成顺序不确定,但是completionBlock回调会在任务全部完成后执行。
2.2.2 第二种写法
- (void)blockOperation {
// 初始化添加事务
NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1————%@",[NSThread currentThread]);
}];
NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务2————%@",[NSThread currentThread]);
}];
NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务3————%@",[NSThread currentThread]);
}];
NSBlockOperation *bo4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务4————%@",[NSThread currentThread]);
}];
// bo1的回调监听
bo1.completionBlock = ^{
NSLog(@"bo1 的completionBlock执行,任务完成");
};
// bo2的回调监听
bo2.completionBlock = ^{
NSLog(@"bo2 的completionBlock执行,任务完成");
};
// bo3的回调监听
bo3.completionBlock = ^{
NSLog(@"bo3 的completionBlock执行,任务完成");
};
// bo4的回调监听
bo4.completionBlock = ^{
NSLog(@"bo4 的completionBlock执行,任务完成");
};
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:bo1];
[queue addOperation:bo2];
[queue addOperation:bo3];
[queue addOperation:bo4];
NSLog(@"事务添加进了NSOperationQueue");
}
--------------------输出结果:-------------------
事务添加进了NSOperationQueue
任务1————<NSThread: 0x2834dd200>{number = 7, name = (null)}
任务4————<NSThread: 0x2834d62c0>{number = 4, name = (null)}
任务2————<NSThread: 0x2834dd7c0>{number = 6, name = (null)}
任务3————<NSThread: 0x2834d6fc0>{number = 5, name = (null)}
bo4 的completionBlock执行,任务完成
bo1 的completionBlock执行,任务完成
bo3 的completionBlock执行,任务完成
bo2 的completionBlock执行,任务完成
--------------------输出结果:-------------------
3. NSOperation添加到NSOperationQueue中
- (void)addOperation:(NSOperation *)op;需要先创建操作,再将创建好的操作加入到创建好的队列中去。- (void)addOperationWithBlock:(void (^)(void))block;无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中。 3.1 使用addOperation将操作加入到操作队列中
- (void)addOperationToQueue {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
// 使用 NSInvocationOperation 创建操作1
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 使用 NSInvocationOperation 创建操作2
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
// 使用 NSBlockOperation 创建操作3
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op3 addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.使用 addOperation: 添加所有操作到队列中
[queue addOperation:op1]; // [op1 start]
[queue addOperation:op2]; // [op2 start]
[queue addOperation:op3]; // [op3 start]
}
3.2 使用addOperationWithBlock将操作加入到操作队列中
- (void)addOperationWithBlockToQueue {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.使用 addOperationWithBlock: 添加操作到队列中
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}
4. maxConcurrentOperationCount 最大并发数
通过设置NSOperationQueue的maxConcurrentOperationCount可以控制串行还是并发执行。
maxConcurrentOperationCount默认情况下为-1,表示不进行限制,可进行并发执行。maxConcurrentOperationCount为1时,队列为串行队列。只能串行执行。maxConcurrentOperationCount大于1时,队列为并发队列。操作并发执行,真正执行的时候,并发数不会超过系统限制。
5. 添加依赖
在NSOperation中通过addDependency添加依赖能很好的控制任务执行的先后顺序
- (void)operationQueue {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"请求token");
}];
NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"拿着token,请求数据1");
}];
NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"拿着数据1,请求数据2");
}];
[bo2 addDependency:bo1];
[bo3 addDependency:bo2];
[self.queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
NSLog(@"全部执行完毕");
}
--------------------输出结果:-------------------
请求token
拿着token,请求数据1
拿着数据1,请求数据2
全部执行完毕
--------------------输出结果:-------------------
6. 设置优先级
NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。NSOperation设置优先级只会让CPU有更高的几率调用,不是说设置高就一定全部先完成。
- (void)blockOperation {
NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
//sleep(1);
NSLog(@"第一个操作 %d --- %@", i, [NSThread currentThread]);
}
}];
// 设置最高优先级
bo1.qualityOfService = NSQualityOfServiceUserInteractive;
NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
NSLog(@"第二个操作 %d --- %@", i, [NSThread currentThread]);
}
}];
// 设置最低优先级
bo2.qualityOfService = NSQualityOfServiceBackground;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:bo1];
[queue addOperation:bo2];
}
- 不使用sleep——高优先级的任务1先于低优先级的任务2
- 使用sleep进行延时——高优先级的任务1慢于低优先级的任务2
1、如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。
2、优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。例如: 一个队列中包含operation1和operation2,operation1依赖operation2,但是operation1的优先级比operation2的优先级高, 结果是operation2优先执行。因为queuePriority属性决定的是进入准备就绪状态下的操作之间的开始执行顺序。
7. 任务的挂起、继续、取消
// 挂起
queue.suspended = YES;
// 继续
queue.suspended = NO;
// 取消
[queue cancelAllOperations];
- 暂停操作不能使当前正在处于执行状态的任务暂停,而是该任务执行结束,后面的任务不会执行,处于排队等待状态。
- 取消操作跟暂停相似,当前正在执行的任务不会立即取消,而是后面的所有任务永远不再执行,且该操作是不可以恢复的。
8. NSOperation和NSOperationQueue 常用属性和方法归纳
8.1 NSOperation 常用属性和方法
-
取消操作方法
- (void)cancel;可取消操作,实质是标记 isCancelled 状态。
-
判断操作状态方法
- (BOOL)isFinished;判断操作是否已经结束。- (BOOL)isCancelled;判断操作是否已经标记为取消。- (BOOL)isExecuting;判断操作是否正在在运行。- (BOOL)isReady;判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。
-
操作同步
- (void)waitUntilFinished;阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。- (void)setCompletionBlock:(void (^)(void))block;completionBlock会在当前操作执行完毕时执行 completionBlock。- (void)addDependency:(NSOperation *)op;添加依赖,使当前操作依赖于操作 op 的完成。- (void)removeDependency:(NSOperation *)op;移除依赖,取消当前操作对操作 op 的依赖。@property (readonly, copy) NSArray<NSOperation *> *dependencies;在当前操作开始执行之前完成执行的所有操作对象数组。
8.2 NSOperationQueue 常用属性和方法归纳
-
取消/暂停/恢复操作
- (void)cancelAllOperations;可以取消队列的所有操作。- (BOOL)isSuspended;判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。- (void)setSuspended:(BOOL)b;可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
-
操作同步
- (void)waitUntilAllOperationsAreFinished;阻塞当前线程,直到队列中的操作全部执行完毕。
-
添加/获取操作
- (void)addOperationWithBlock:(void (^)(void))block;向队列中添加一个 NSBlockOperation 类型操作对象。- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束- (NSArray *)operations;当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。- (NSUInteger)operationCount;当前队列中的操作数。
-
获取队列
+ (id)currentQueue;获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。+ (id)mainQueue;获取主队列。
三、应用
1. 线程间通讯
在GCD中使用异步进行网络请求,然后回到主线程刷新UI.NSOperation中也有类似在线程间通讯的操作
- (void)operationQueue {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = @"test";
[queue addOperationWithBlock:^{
NSLog(@"请求网络%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"刷新UI%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
}];
}];
}
2. 线程安全
模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。
场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。
/**
* 线程安全:使用 NSLock 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
self.ticketSurplusCount = 50;
self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象
// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;
// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;
// 3.创建卖票操作 op1
__weak typeof(self) weakSelf = self;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];
// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];
// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 加锁
[self.lock lock];
if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}
// 解锁
[self.lock unlock];
if (self.ticketSurplusCount <= 0) {
NSLog(@"所有火车票均已售完");
break;
}
}
}
3. 自定义NSOperation缓存机制
根据SDWebImage加载网络图片的缓存机制,用NSOperation自定义图片缓存(本地缓存+内存缓存)
- 如果内存中有数据,则从内存中取出图片来展示
- 如果沙盒中有数据,则从沙盒中取出图片来展示并存一份到内存中
- 如果都没有就异步下载把图片数据写到本地缓存和内存缓存中
-(void)simulationCacheImage{
UIImage *cacheImage = self.imageCacheDict[model.imageUrl];
if (cacheImage) {
NSLog(@"从内存获取图片:%@", model.title);
cell.imageView.image = cacheImage;
return cell;
}
UIImage *diskImage = [UIImage imageWithContentsOfFile:[model.imageUrl getDowloadImagePath]];
if (diskImage) {
NSLog(@"从沙盒获取image:%@",model.title);
cell.imageView.image = diskImage;
[self.imageCacheDict setValue:diskImage forKey:model.imageUrl];
return cell;
}
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"去下载图片:%@", model.title);
// 延迟
NSData *data = [NSData dataWithContentsOfURL:imageURL];
UIImage *image = [UIImage imageWithData:data];
// 存内存
[self.imageCacheDict setValue:image forKey:model.imageUrl];
[data writeToFile:[model.imageUrl getDowloadImagePath] atomically:YES];
// 更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
cell.imageView.image = image;
}];
}];
[self.queue addOperation:bo];
}