文章目录
NSOperation、NSOperationQueue
NSOperation、NSOperationQueue是苹果提供给我们的一套多线程解决方案。
实际上NSOperation、NSOperationQueue是基于GCD更高一层的封装,完全面向对象。
比GCD更简单易用、代码可读性也更高
为什么要使用NSOperation、NSOperationQueue
- 可添加完成的代码块,在操作完成之后执行
NSOperation的completionBlock属性 - 添加操作之间的依赖关系,方便的控制执行顺序
- 设定操作执行的优先级
- 可以很方便的取消一个操作的执行
- 使用KVO观察对操作执行状态的更改executing、finished、cancelled
通过KVO的方式移除finished值为yes的NSOperation - 可设置最大并发操作数,来控制串行、并行
NSOperation、NSOperationQueue操作和操作队列
NSOperation是基于GCD的更高一层的封装,GCD中的一些概念同样适用于NSOperation、NSOperationQueue。
在NSOperation、NSOperationQueue中也有类似任务(操作)、队列(操作队列)的概念
- 操作(Operation)
- 执行操作的意思,就是在线程中执行的那段代码
- 在GCD中是放在Block中的。在NSOperation中,我们使用NSOperation子类NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作
- 操作队列(Operation Queues)
- 操作队列,即用来存放操作的队列。不同于GCD中的调度队列FIFO(先进先出)的原则。NSOperationQueue对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于与操作之间的依赖关系),然后进入就绪状态的这些操作,开始执行的顺序由操作之间的优先级决定。(优先级是操作对象自身的属性)
NSOperation、NSOperationQueue使用步骤
NSOperation实现多线程的使用步骤分为三步:
- 创建操作:先将需要执行的操作封装到一个NSOperation对象中
- 创建队列:创建NSOperationQueue对象
- 将操作加入到队列中:将NSOperation对象添加到NSOperationQueue对象中
之后,系统会自动将NSOperationQueue中的NSOperation取出来,在新线程中执行操作
NSOperation、NSOperation基本使用
创建操作
NSOperation是个抽象类,不能创建实例,不能用来封装操作。我们只能要他的子线程来封装操作。有三种方式来封装操作。
- 使用子类NSInvocationOperation
- 使用子类NSBlockOperation
- 自定义继承自NSOperation的子类,通过实现内部相应的方法来封装操作
使用子类NSInvocationOperation

- 在没有使用NSOperationQueue、在主线程中使用子类NSInvocationOperation执行一个操作的情况下,操作时在当前线程中执行的。并没有开启新线程。
- 如果在其他线程中执行操作,则打印的结果为其他线程

- 在其他线程中单独使用子类NSInvocationOperation,操作是在这个其他线程中执行的,并没有开启新线程
使用子类NSBlockOperation

- 可以看到在没有使用NSOperationQueue、在主线程中单独使用NSBlockOperation执行一个操作的情况下,操作是在当前线程执行的,没有开启新线程。
当然,如果是在其他线程使用NSBlockOperation执行一个操作,打印结果就为其他线程。同样也没有开启新线程。
NSBlockOperation还有一个方法addExecutionBlock:,通过addExecutionBlock:就可以为NSBlockOperation添加额外的操作。这些操作(包括blockOperationWithBlock中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。
如果添加的操作多的话,blockOperationWithBlock:中的操作也有可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到blockOperationWithBlock:中的操作一定在当前线程中执行。
- (void)useBlockOperation {
NSLog(@"%@", [NSThread currentThread]);
//1 创建NSBlockOperation对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
int i = 2;
while (i--) {
[NSThread sleepForTimeInterval:2];
NSLog(@"Block task, %@", [NSThread currentThread]);
}
}];
//2 添加额外的操作
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"1--Block task, %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2--Block task, %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3--Block task, %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4--Block task, %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"5--Block task, %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"6--Block task, %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"7--Block task, %@", [NSThread currentThread]);
}];
//2 调用start方法开始执行操作
[op start];
}

- 使用子类NSBlockOperation,并调用addexecutionBlock:的情况下,blockOperationWithBlock:方法中的操作和额外加的操作是在不同线程中异步执行的。同时,额外操作多的时候,blockOperationWithBlock:方法中的操作有可能不会在当前线程中执行,比说上面的例子。
所以blockOperationWithBlock:中的操作也有可能在其他线程(非当前线程)中执行 - 开启的线程数是由系统来决定的。
使用继承自NSOperation自定义子类
我们可以自定义继承自NSOperation的子类。可通过重写main或者start来定义自己的NSOperation对象。
- 如果只是重写了main方法,有底层控制变更任务执行、完成状态以及任务退出
- 如果重写了start方法,需要自己控制任务状态
重写main方法比较简单,我们不需要管理线程的状态属性executing(是否正在执行)和finished(是否完成)。当main执行完返回的时候,这个操作就结束了
//.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SubOperation : NSOperation
@end
//.m文件
#import "SubOperation.h"
@implementation SubOperation
- (void)main {
NSLog(@"%d", self.finished);
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
@end
//viewDidload中
SubOperation *op = [[SubOperation alloc] init];
NSLog(@"%d", op.finished);
[op start];
NSLog(@"%d", op.finished);

没有使用NSOperationQueue,在主线程中创建自定义子类的操作对象并执行,并没有开启新线程。
如果是在其他线程中执行这个子类操作,同样也是没有开启新线程,执行操作的线程还是为当前线程
创建队列
NSOperationQueue共有两种队列:主队列、自定义队列
其中自定义队列同时包含了串行、并发功能。
- 主队列:凡是添加到主队列中的操作,都会放到主线程中执行
- 自定义队列(非主队列)
添加到这种队列中的操作,会自动放到子线程中执行
同时包含了 串行、并发 功能
//主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
//自定义队列(非主队列)
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
将操作加入队列中
NSOperation需要配合NSOperationQueue来实现多线程
将创建好的操作加入到队列中,有两种方法
- (void)addOperation:(NSOperation *)op;
创建好队列、操作,再将操作都加到队列中

此时可以看到,使用NSOperation子类创建的操作,并使用addOperation:将操作加入到操作队列中后就能开启新线程,进行并发执行。- (void)addOperationWithBlock:(void (^)(void)block);
不用创建操作,直接在block中添加操作,将block加入到队列中

使用addOperationWithBlock:将操作加入到操作队列中后同样是开启新线程,进行并发执行。
这时可能有点疑惑,自定义队列同时具有串行、并行,现在怎么都是并行…
NSOperationQueue 控制串行执行、并行执行
操作队列有一个属性,最大并发操作数
用来控制一个特定的队列中可以有多少个操作同时并发执行,也就是一个队列中同时能并发执行的最大操作数。
- maxConcurrentOperationCount默认为-1,表示不进行限制,可进行并发执行
- maxConcurrentOperationCount为1时,此时队列为串行队列,只能串行执行
- maxConcurrentOperationCount大于1时,队列为并发队列。操作并发执行。当这个值超过了系统限制,就会自动调整为系统设定的值。

最大并发数为1的输出结果:
当最大并发操作数为1时,操作是按顺序串行执行的,一个操作执行完之后下一个操作才开始执行。
最大并发数为2的输出结果:
当最大并发操作数为2时,操作是并发执行的,可同时执行两个操作。
对于开启线程数,是由系统决定的,不需要我们来管理
NSOperation操作依赖
NSOperation、NSOperationQueue最吸引人的就是它能够添加操作之间的依赖关系,通过依赖关系,我们就可以很方便的控制操作之间的执行先后顺序。
NSOperation提供了3个接口供我们使用依赖
- (void)addDependency:(NSOperation *)op添加依赖,是当前操作依赖操作op的完成,op完成之后才会执行当前操作- (void)removeDependency:(NSOperation *)op移除依赖,取消当前操作对操作op的依赖@property (readonly, copy) NSArray<NSOperation *> *dependencies;操作对象的一个属性,在当前操作开始执行之前完成执行数组中的所有操作对象
没有添加依赖时,并没有按着想要的结果打印
添加依赖后
如果使op1、op2相互依赖,此时op1、op2都不会执行
NSOperation优先级
NSOperation提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。
对于添加到队列中的操作,首先进入准备就绪的状态、,然后进入就绪状态的操作的开始执行顺序由操作之间的相对的优先级决定(优先级时操作对象自身的属性)
就绪状态取决于操作之间的依赖关系
进入就绪状态的操作也就是这个操作的所有依赖已经完成时,这个操作对象通常会进入准备就绪状态,等待执行。
当有四个优先级都是NSOperationQueuePriorityNormal(默认优先级)的操作:op1、op2、op3、op4,op2依赖于op3,op3依赖于op4。
- 其中只有op1、op4没有需要依赖的操作,所以op1、op4就是处于准备就绪状态的操作
- op2、op3都有依赖的操作,所以op2、op3都不是准备就绪的操作
- 当op4完成时,op3进入就绪状态;当op3完成时,op2进入就绪状态
queuePriority:
- 而queuePriority属性决定了已进入就绪状态下的操作之间的开始执行顺序。优先级不能取代依赖关系。该属性仅决定开始执行顺序,并不能保证完成执行顺序
- 如果一个队列中既包含高优先级操作、也有低优先级操作,并且这两个操作都已经准备就绪,那么队列就会先执行高优先级操作。
- queuePriority属性决定的是进入就绪状态下的操作之间的开始执行顺序,并不保证执行完成顺序。而依赖则是控制两个操作之间的执行顺序,使一个操作可在它依赖的操作执行完成之后再开始执行。
- 如果将最大操作执行数设置为1

- 将最大操作执行数设置为1,那么队列中操作数将会串行执行,一个操作执行完才会执行另一个操作。
- 队列中的操作根据其就绪状态、优先级、依赖关系进行组织并进行相应的执行。
- 如果队列中所有的操作的优先级相同,并且也进入就绪状态,那么执行的顺序就按照提交到队列的顺序执行。否则,队列总是执行相对于其他就绪操作优先级更高的操作。
- 但奇怪的是第一个添加到队列中的已就绪操作,就会第一个执行,不会考虑优先级
?????????
如果我们要确保操作的执行的先后顺序,即操作间有同步关系,我们应该使用依赖关系来确保绝对的执行顺序,即使操作对象位于不同的操作队列中。在操作对象的所有依赖操作完成执行之前,操作对象不会被视为已准备好执行。
NSOperation、NSOperationQueue线程间的通信
在iOS开发过程中,我们一般要在主线程中进行UI刷新。通常把一些耗时的操作放在其他线程中,当其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程间的通信

NSOperation 操作 常用属性和方法
//操作是否取消
@property (readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel; //可取消操作,实质是标记isCancelled状态
//操作是否结束
@property (readonly, getter=isExecuting) BOOL executing;
//操作是否完成
@property (readonly, getter=isFinished) BOOL finished;
//操作是否处于就绪状态 与操作的依赖有关
@property (readonly, getter=isReady) BOOL ready;
//操作的优先级
@property NSOperationQueuePriority queuePriority;
//优先级枚举
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
//会在当前操作执行完毕执行completionBlock
@property (nullable, copy) void (^completionBlock)(void) API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
//阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步
- (void)waitUntilFinished API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
//添加依赖 时当前操作依赖于操作op的完成
- (void)addDependency:(NSOperation *)op;
- //移除依赖
- (void)removeDependency:(NSOperation *)op;
NSOperationQueue 操作队列 常用属性和方法
//最大操作执行数
@property NSInteger maxConcurrentOperationCount;
//添加操作
- (void)addOperation:(NSOperation *)op;
//向队列中添加操作数组,wait标志是否阻塞当前线程直到数组中所有操作结束
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
//向队列中添加一个NSBlockOperation类型操作对象
- (void)addOperationWithBlock:(void (^)(void))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
//操作同步
//阻塞当前线程,直到队列中的操作全部执行完毕
- (void)waitUntilAllOperationsAreFinished;
//获取队列
//当前队列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
//主队列
@property (class, readonly, strong) NSOperationQueue *mainQueue API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
//取消操作
- (void)cancelAllOperations;
//暂停状态。yes为暂停,no为恢复状态
- @property (getter=isSuspended) BOOL suspended;
暂停和取消,(取消包括操作的取消和队列的取消)并不代表当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作
队列中的暂停和取消的区别在于:暂停操作之后还可以恢复操作,向下继续执行。而取消操作,所有的操作从操作队列中清空,无法再接着执行剩下的操作。
GCD对比NSOperationQueue
首先,明确一下NSOperationQueue与GCD之间的关系
GCD的实现是C语言,NSOperationQueue是基于GCD更高一层的封装,是GCD的高级抽象
- GCD的执行效率更高,而且由于队列中执行的是由Block构成的任务,这是一个轻量级的数据结构,写起来更加方便
- GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大操作并发数、设置优先级、添加依赖关系来调整执行顺序
- NSOperationQueue甚至可以跨队列设置依赖关系,但GCD只能通过设置串行队列,或者在队列内添加栅栏方法执行任务,才能控制执行顺序,较为复杂
- NSOperationQueue因为更加面向对象,所以支持KVO,可以检测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCancelled)
在实际的开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的优化完善、运行快速的GCD是首选
如果考虑异步操作之间的顺序行、依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持
不论是GCD还是NSOperationQueue,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理不需要我们来操心,系统对于线程的创建、调度管理和释放都做的很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销
放一张小码哥的图