NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
1.为什么要使用 NSOperation、NSOperationQueue?
- 可添加完成的代码块,在操作完成后执行。
- 添加操作之间的依赖关系,方便的控制执行顺序。
- 设定操作执行的优先级。
- 可以设置最大并发数。
- 可以很方便的取消一个操作的执行。
- 使用 KVO 观察对操作执行状态的更改:
isExecuteing、isFinished、isCancelled
2.常用类
既然是基于 GCD 的更高一层的封装。那么,GCD 中的一些概念同样适用于 NSOperation、NSOperationQueue。在 NSOperation、NSOperationQueue 中也有类似的任务(操作)和队列(操作队列)的概念。
-
操作(Operation):
- 执行操作的意思,换句话说就是你在线程中执行的那段代码。
- 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
-
操作队列(Operation Queues):
- 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
- 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
- NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
3.NSOperation的使用
NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。
⚠️在不使用 NSOperationQueue,单独使用 NSOperation 的情况下,是 同步操作
⚠️如果自定义继承自 NSOperation 的子类,重写start
方法在子线程中进行,可以实现异步操作
1). 使用子类 NSInvocationOperation
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget: self
selector: @selector(task1)
object: nil];
// 2.调用 start 方法开始执行操作
[op start];
在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
2). 使用子类 NSBlockOperation
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}];
// 2.调用 start 方法开始执行操作
[op start];
在没有使用 NSOperationQueue、在主线程中单独使用 NSBlockOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
NSBlockOperation还可以添加额外的操作:
[op addExecutionBlock:^{
}];
[op addExecutionBlock:^{
}];
[op start];
使用子类 NSBlockOperation,并调用方法 AddExecutionBlock:
的情况下,blockOperationWithBlock:
方法中的操作 和addExecutionBlock:
中的操作有可能均在新开启的线程中进行。
NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。
3). 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。
我们可以自定义非并发和并发两种不同类型的 NSOperation 子类。
从最低限度上来说,每一个 operation 都应该至少实现以下两个方法:
- 一个自定义的初始化方法,将创建的 operation 置于一个已知的状态
- 重写 main 方法来执行我们的任务
例如:
- (id)initWithData:(id)data {
if ([super init]) {
}
return self;
}
- (void)main {
}
自定义NSOperation支持取消
当一个 operation 开始执行后,它会一直执行它的任务直到完成或被取消为止。我们可以在任意时间点取消一个 operation ,甚至是在它还未开始执行之前。
为了让我们自定义的 operation 能够支持取消事件,我们需要在代码中定期地检查isCancelled
方法的返回值,一旦检查到这个方法返回 YES ,我们就需要立即停止执行接下来的任务。
通常来说,当我们自定义一个 operation 类时,我们需要考虑在以下几个关键点检查 isCancelled 方法的返回值:
- 在真正开始执行任务之前;
- 至少在每次循环中检查一次,而如果一次循环的时间本身就比较长的话,则需要检查得更加频繁;
- 在任何相对来说比较容易中止 operation 的地方。
看到这里,我想你应该可以意识到一点,那就是尽管 operation 是支持取消操作的,但却并不是立即取消的,而是在你调用了 operation 的 cancel 方法之后的下一个 isCancelled 的检查点取消的。
- (void)main {
if (self.isCancelled) return;
for (NSUInteger i = 0; i < 3; i++) {
if (self.isCancelled) return;
NSLog(@"Loop %@", @(i + 1));
}
}
配置并发的自定义NSOperation
在 operation 和 operation queue 结合使用时,operation queue 可以为非并发的 operation 提供线程,但是,如果你想要手动地执行一个 operation ,又想这个 operation 能够异步执行的话,你需要做一些额外的配置来让你的 operation 支持并发执行。
下面列举了一些你可能需要重写的方法:
-
start
:必须的。
所有并发执行的 operation 都必须要重写这个方法,替换掉 NSOperation 类中的默认实现。start 方法是一个 operation 的起点,我们可以在这里配置任务执行的线程或者一些其它的执行环境。
另外,需要特别注意的是,在我们重写的 start 方法中一定不要调用父类的实现; -
main
:可选的。
通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start 方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰; -
isExecuting
和isFinished
:必须的。
并发执行的 operation 需要负责配置它们的执行环境,并且向外界客户报告执行环境的状态。因此,一个并发执行的 operation 必须要维护一些状态信息,用来记录它的任务是否正在执行,是否已经完成执行等。
此外,当这两个方法所代表的值发生变化时,我们需要生成相应的 KVO 通知,以便外界能够观察到这些状态的变化; -
isConcurrent
:必须的。
这个方法的返回值用来标识一个 operation 是否是并发的 operation ,我们需要重写这个方法并返回 YES 。
例如:
@synthesize executing = _executing;
@synthesize finished = _finished;
- (id)init {
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (void)start {
if (self.isCancelled) {
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
// 任务真正开始执行
- (void)main {
@try {
NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]);
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
[self didChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
}
@catch (NSException *exception) {
NSLog(@"Exception: %@", exception);
}
}
注意:
- 与重写 main 方法不同的是,如果我们重写了 start 方法或者对 NSOperation 类做了大量定制的话,我们需要保证自定义的 operation 在这些 key paths 上仍然支持 KVO 通知。比如,当我们重写了 start 方法时,我们需要特别关注的是 isExecuting 和 isFinished 这两个 key paths ,因为这两个 key paths 最可能受重写 start 方法的影响。
- 即使一个 operation 是被 cancel 掉了,我们仍然需要手动触发 isFinished 的 KVO 通知。
因为当一个 operation 依赖其他 operation 时,它会观察所有其他 operation 的 isFinished 的值的变化,只有当它依赖的所有 operation 的 isFinished 的值为 YES 时,这个 operation 才能够开始执行。
因此,如果一个我们自定义的 operation 被取消了但却没有手动触发 isFinished 的 KVO 通知的话,那么所有依赖它的 operation 都不会执行。
NSOperation 类的以下 key paths 支持 KVO 通知,我们可以通过观察这些 key paths 非常方便地监听到一个 operation 内部状态的变化:
- isCancelled
- isConcurrent
- isExecuting
- isFinished
- isReady
- dependencies
- queuePriority
- completionBlock
附:
虽然我们可以同时使用NSOperation和NSOperationQueue来创建并发任务,
但是我们也可以单独使用NSOperation实现:
创建NSOperation子类,并且在start
方法中判断当前Operation是否是并发操作
- (void)start {
if (operation.isCancelled) {
ranIt = YES;
} else if (operation.isReady) {
if (!operation.isConcurrent) {
[operation start];
} else {
[NSThread detachNewThreadSelector:@selector(start) toTarget:operation withObject:nil];
}
ranIt = YES;
}
}
4).设置CompletionBlock
一个 operation 可以在它的主任务执行完成时回调一个 completion block 。我们可以用 completion block 来执行一些主任务之外的工作,比如,我们可以用它来通知一些客户 operation 已经执行完毕,而并发的 operation 也可以用这个 block 来生成最终的 KVO 通知。如果需要设置一个 operation 的 completion block ,直接调用 NSOperation 类的 setCompletionBlock: 方法即可。
注意,当一个 operation 被取消时,它的 completion block 仍然会执行,所以我们需要在真正执行代码前检查一下 isCancelled 方法的返回值。另外,我们也没有办法保证 completion block 被回调时一定是在主线程,理论上它应该是与触发 isFinished 的 KVO 通知所在的线程一致的,所以如果有必要的话我们可以在 completion block 中使用 GCD 来保证从主线程更新 UI 。
5).取消、暂停
一旦一个 operation 被添加到 operation queue 后,唯一从 operation queue 中出队一个 operation 的方式就是调用它的 cancel
方法取消这个 operation ,或者直接调用 operation queue 的 cancelAllOperations
方法取消这个 operation queue 中所有的 operation 。
另外,我们前面也提到了,当一个 operation 被取消后,这个 operation 的 isFinished
状态也会变成 YES ,这样处理的好处就是所有依赖它的 operation 能够接收到这个 KVO 通知,从而能够清除这个依赖关系正常执行。
如果我们想要暂停和恢复执行 operation queue 中的 operation ,可以通过调用 operation queue 的 setSuspended:
方法来实现这个目的。不过需要注意的是,暂停执行 operation queue 并不能使正在执行的 operation 暂停执行,而只是简单地暂停调度新的 operation 。另外,我们并不能单独地暂停执行一个 operation ,除非直接 cancel 掉。
4.NSOperationQueue和NSOperation配合使用
1). 设置最大并发数
NSOperationQueue的属性 maxConcurrentOperationCount
最大并发操作数,用来控制一个特定队列中可以有多少个操作同时参与并发执行。
-
maxConcurrentOperationCount
默认情况下为-1,表示不进行限制,可进行并发执行。 -
maxConcurrentOperationCount
为1时,队列为串行队列。只能串行执行。 -
maxConcurrentOperationCount
大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1; // 串行队列
// queue.maxConcurrentOperationCount = 2; // 并发队列
// queue.maxConcurrentOperationCount = -1; // 默认值, 并发队列
[queue addOperationWithBlock:^{
}];
[queue addOperationWithBlock:^{
}];
2).设置操作依赖
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
}];
// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
3).设置优先级
NSOperation 提供了queuePriority
(优先级)属性,queuePriority属性适用于 同一操作队列 中的操作,不适用于不同操作队列中的操作。
当一个操作的所有依赖都已经完成时,即isReady
为YES,操作对象通常会进入准备就绪状态,等待执行。
queuePriority
属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。
如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。
5.线程间通信
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// 2.添加操作
[queue addOperationWithBlock:^{
// 异步进行耗时操作
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操作
}];
}];