NSOperation
、NSOperationQueue
是苹果提供给我们的一套多线程解决方案。实际上 NSOperation
、NSOperationQueue
是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
NSOperation是基于GCD的更高一层的封装,GCD中的一些概念同样适用于NSOperation、NSOperationQueue。
在NSOperation、NSOperationQueue中也有类似任务(操作)、队列(操作队列)的概念:
cancelAllOperations
;取消队列中所有的操作isSuspended
判断队列是否处理暂停状态 YES:暂停状态,NO恢复状态setSuspended:(BOOL)b
;设置操作的暂停和恢复 YES:暂停,NO:恢复waitUntilAllOperationsAreFinished
;阻塞当前线程,直到队列中的操作全部完成addOperationWithBlock:(void (^)(void))block
向队列中添加一个NSBlockOperation类型的操作对象addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait
;添加操作数组,wait标志是否阻塞当前线程知道所有操作结束operations
当前在队列中的操作数组operationCount
操作个数currentQueue
当前队列,如果当前线程不在Queue上,返回nilmainQueue
获取主队列暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。
NSOperation实现多线程的使用步骤分为三步:
之后,系统会自动将NSOperationQueue中的NSOperation取出来,在新线程中执行操作。
因为NSOperation是个抽象类,不能创建实例,所以我们通常使用它的子类来进行封装操作:
- (void)viewDidLoad {
[super viewDidLoad];
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testOp) object:nil];
// 2.调用 start 方法开始执行操作
[op start];
}
- (void)testOp {
NSLog(@"testOp--%@", [NSThread currentThread]);
}
使用子类NSBlockOperation
NSLog(@"----%@", [NSThread currentThread]); // 打印当前线程
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"%d---%@", i, [NSThread currentThread]); // 打印当前线程
}
}];
// 2.调用 start 方法开始执行操作
[op start];
输出结果:
可以看到:在没有使用 NSOperationQueue
、在主线程中单独使用 NSBlockOperation
执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
注意: 和上边 NSInvocationOperation
使用一样。因为代码是在主线程中调用的,所以打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程。
但是,NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。
如果添加的操作多的话, blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。(可以使用 addExecutionBlock: 多添加几个操作试试):
- (void)useBlockOperationAddExecutionBlock {
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.添加额外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.调用 start 方法开始执行操作
[op start];
}
运行结果:
可以看出:使用子类 NSBlockOperation,并调用方法 AddExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中异步执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了 blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。
一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。
如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。重写main方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished。当 main 执行完返回的时候,这个操作就结束了
先定义一个继承自 NSOperation 的子类,重写main方法。
//
// MYOperation.m
// NSOperation学习
//
// Created by 翟旭博 on 2023/4/5.
//
#import "MYOperation.h"
@implementation MYOperation
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
}
@end
我们运行下康康:
可以看出:在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类的情况下,是在主线程执行操作,并没有开启新线程。
另外我们再尝试一下start方法的重写:
- (void)start {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
[super start];
}
NSOperationQueue共有两种队列:主队列、自定义队列,其中自定义队列同时包含了串行、并发功能。
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 自定义队列(非主队列)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSOperation需要配合NSOperationQueue才能实现多线程,将创建好的操作加入到队列中,有两种方法:
1.- (void)addOperation:(NSOperation *)op;方法
创建好队列、操作,再将操作都加到队列中:
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
NSInvocationOperation *firstOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationFirst) object:nil];
NSBlockOperation *secondOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2---%@", [NSThread currentThread]);
}];
[secondOperation addExecutionBlock:^{
NSLog(@"add--%@", [NSThread currentThread]);
}];
NSInvocationOperation *thirdOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationThird) object:nil];
// 3.将操作加到队列中
[queue addOperation:firstOperation];
[queue addOperation:secondOperation];
[queue addOperation:thirdOperation];
.......
- (void)operationFirst {
NSLog(@"1---%@", [NSThread currentThread]);
}
- (void)operationThird {
NSLog(@"3---%@", [NSThread currentThread]);
}
2.- (void)addOperationWithBlock:(void (^)(void)block);
不用创建操作,直接在block中添加操作,将block加入到队列中:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2---%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3---%@", [NSThread currentThread]);
}];
输出结果:
与上述方法相同,不过使用block看起来更简洁了。
操作队列有一个属性,最大并发操作数,用来控制一个特定的队列中可以有多少个操作同时并发执行,也就是一个队列中同时能并发执行的最大操作数:
@property NSInteger maxConcurrentOperationCount;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2; // 控制一次最多执行的线程数
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2---%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3---%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4---%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2];
NSLog(@"5---%@", [NSThread currentThread]);
}];
输出结果:
queue.maxConcurrentOperationCount = 1;
queue.maxConcurrentOperationCount = 2;
queue.maxConcurrentOperationCount = -1;
可能从输出结果上看不出什么,主要是输出的过程,你可以自己用上述代码试试,当为 1 的时候,明显能看出来每个操作之间都有2秒的延时,但是为 2 的时候,它延迟两秒之后几乎同时输出的。
对于开启线程数,是由系统决定的,不需要我们来管理。
NSOperation、NSOperationQueue最吸引人的就是它能够添加操作之间的依赖关系,通过依赖关系,我们就可以很方便的控制操作之间的执行先后顺序。
NSOperation提供了3个接口供我们使用依赖
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *firstOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"firstOperation");
}];
NSBlockOperation *secondOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"secondOperation");
}];
NSBlockOperation *thirdOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"thirdOperation");
}];
[queue addOperation:firstOperation];
[queue addOperation:secondOperation];
[queue addOperation:thirdOperation];
输出结果:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *firstOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"firstOperation");
}];
NSBlockOperation *secondOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"secondOperation");
}];
NSBlockOperation *thirdOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"thirdOperation");
}];
[secondOperation addDependency:firstOperation]; // 让secondOperation依赖于firstOperation,即firstOperation先执行,在执行secondOperation
[thirdOperation addDependency:secondOperation]; // 让thirdOperation依赖于secondOperation,即secondOperation先执行,在执行thirdOperation
[queue addOperation:firstOperation];
[queue addOperation:secondOperation];
[queue addOperation:thirdOperation];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *firstOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"firstOperation");
}];
NSBlockOperation *secondOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"secondOperation");
}];
NSBlockOperation *thirdOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"thirdOperation");
}];
[secondOperation addDependency:firstOperation]; // 让secondOperation依赖于firstOperation,即firstOperation先执行,在执行secondOperation
[firstOperation addDependency:secondOperation]; // 让firstOperation依赖于secondOperation,即secondOperation先执行,在执行firstOperation
[queue addOperation:firstOperation];
[queue addOperation:secondOperation];
[queue addOperation:thirdOperation];
两个相互依赖之后,都不输出了
依赖只是一种执行关系罢了,NSOperation还为我们专门提供了优先级属性,我们可以通过改变NSOperationQueuePriority属性来设置同一队列中操作的优先级,下面是系统给定的优先级(默认为NSOperationQueuePriorityNormal):
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
}
对于添加到队列中的操作,首先进入准备就绪的状态,然后进入就绪状态的操作的开始执行顺序由操作之间的相对的优先级决定。
就绪状态取决于操作之间的依赖关系,也就是只有这个操作的依赖操作完成了,该操作才会处于就绪状态。
举个例子:
当有四个优先级都是NSOperationQueuePriorityNormal(默认优先级)的操作:op1、op2、op3、op4,op2依赖于op3,op3依赖于op4。
queuePriority:
在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
UIImageView *imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(100, 100, 200, 200);
imageView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:imageView];
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//2.添加操作
[queue addOperationWithBlock:^{
// 1. 获取图片 imageUrl
NSURL *imageUrl = [NSURL URLWithString:@"https://img-blog.csdnimg.cn/d317e3af47424e03bea4572b6fa4b917.png"];
// 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
// 通过二进制 data 创建 image
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[imageView setImage:image]; //UI操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}];
}];
1.取消操作方法
- (void)cancel;
可取消操作,实质是标记 isCancelled 状态2.判断操作状态方法
- (BOOL)isFinished;
判断操作是否已经结束- (BOOL)isCancelled;
判断操作是否已经标记为取消- (BOOL)isExecuting;
判断操作是否正在在运行- (BOOL)isReady;
判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关3.操作同步
- (void)waitUntilFinished;
阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步- (void)setCompletionBlock:(void (^)(void))block;
会在当前操作执行完毕时执行 completionBlock- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op 的完成- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op 的依赖@property (readonly, copy) NSArray *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组1.取消/暂停/恢复操作
- (void)cancelAllOperations;
可以取消队列的所有操作- (BOOL)isSuspended;
判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态- (void)setSuspended:(BOOL)b;
可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列2.操作同步
- (void)waitUntilAllOperationsAreFinished;
阻塞当前线程,直到队列中的操作全部执行完毕3.添加/获取操作
- (void)addOperationWithBlock:(void (^)(void))block;
向队列中添加一个 NSBlockOperation 类型操作对象- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;
向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束- (NSArray *)operations;
当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)- (NSUInteger)operationCount;
当前队列中的操作数4.获取队列
+ (id)currentQueue;
获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil+ (id)mainQueue;
获取主队列注意:
pthread跨平台,使用难度大,需要手动管理线程生命周期
所以如果我们需要考虑异步操作之间的顺序行、依赖关系,比如多线程并发下载等等,就使用NSOperation。