多线程方案 NSOperation 是基于 GCD 开发的,几乎完全面向对象,因而与 GCD 比较其更加灵活、可扩展、可控以及代码可读性更强。例如:可添加操作之间的依赖关系,方便的控制执行顺序;可设定操作执行的优先级;可以很方便的取消一个操作的执行;可通过 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled等;

NSOperation 初步认识

实际开发中无法直接使用 NSOperation,因为其只是一个抽象基类并不具备封装操作的能力,实际开发中主要使用其两个子类:NSInvocationOperation 和 NSBlockOperation,或者是自定义一个子类; NSOperation 的子类结合 NSOperationQueue 实现多线程即通过操作+队列实现。

  • 操作 (NSOperation 子类):执行操作就是线程中执行的那段代码,可以理解为 GCD 中的 Block 块任务,但是又不能等同,因为一个操作中可以 add 多个任务;
  • 操作队列 (NSOperationQueue):用来存放操作的队列,操作 add 进相应队列后系统会⾃动将 NSOperationQueue 中的 NSOperation 任务取出,放入新线程中执⾏;

NSOperation 操作执行状态

NSOperation 每一个操作的执行状态过程:isReady → isExecuting → isFinished;这里并不是通过一个 state 属性标识,而是直接由上面那些 keypath 的 KVO 通知决定,也就是说,当一个操作在准备好被执行的时候,它发送了一个 KVO 通知给 isReady 的 keypath,让这个 keypath 对应的属性 isReady 在被访问的时候返回 YES;

  • isReady: 返回 YES 表示操作已经准备好被执行, 如果返回 NO 则说明还有其他没有先前的相关步骤没有完成。
  • isExecuting: 返回 YES 表示操作正在执行,反之则没在执行。
  • isFinished : 返回 YES 表示操作执行成功或者被取消了,NSOperationQueue 只有当它管理的所有操作的 isFinished 属性全标为 YES 以后操作才停止出列,也就是队列停止运行,所以正确实现这个方法对于避免死锁很关键。

NSOperation 取消操作

NSOperation 操作执行之前可以通过 Operation 对象调用 cancel 方法取消某个相关操作,取消原因可能是需求需要或者是某个操作失败,在 GCD 中如果是已经加入到队列的则无法取消了;这里 NSOperation 对象调用 cancel 方法的时候会通过 KVO 通知 isCancelled 的 keypath 来修改 isCancelled 属性的返回值;

NSOperation 操作优先级设置

NSOperation 是可以通过设置其 queuePriority 属性来指定操作的优先级,而 GCD 只有队列才有优先级,也相对更加灵活;但是这里的优先级,只是会让 CPU 有更高的几率调用先, 并不是说设置高就一定全部先完成再执行后面的。

  • NSOperationQueuePriorityVeryHigh
  • NSOperationQueuePriorityHigh
  • NSOperationQueuePriorityNormal
  • NSOperationQueuePriorityLow
  • NSOperationQueuePriorityVeryLow

NSOperation 操作设置依赖

NSOperation 可以设置多个操作之间的依赖关系,等 A 操作执行完再执行 B 操作这样,GCD 中如果要实现这种场景只用通过复杂的 dispatch_barrier_async 分割任务执行顺序实现或者是通过 dispatch_semaphore 信号量实现;

NSOperation 操作完成的 Block 回调

NSOperation 提供了一个操作完成的 completionBlock 属性,可以更加方便监听操作完成时机;

1
2
3
operation.completionBlock = ^{
// 操作执行完成回调,这里可以做数据处理 或者 是回到主线程操作相关等等
};

NSOperation 子类配合 NSOperationQueue 实现多线程

Operation 操作单独使用的话,也就是手动调用 start 方法的话操作默认是在当前线程执行不会开启新线程;配合 NSOperationQueue 使用将操作添加进队列后,系统会执行操作,并且是在新线程中。当然如果是 [NSOperationQueue mainQueue] 获取主队列,在主队列中执行的话就不会开启新线程了,只是回到主线程执行。

NSOperation 实现多线程的使用步骤:

  1. 创建操作:是一个 NSOperation 子类对象创建过程,并且添加任务到操作中;
  2. 创建队列:创建 NSOperationQueue 对象过程;
  3. 操作加入队列:将创建好的 NSOperation 子类对象添加入已创建队列中;
  4. 操作完成监听:调用 completionBlock 属性监听操作完成回调;

NSInvocationOperation 实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)InvocationOperation {
// 1. 创建操作 NSInvocationOperation
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(executeOperation:) object:@"zeroccOperation"];
// 2. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 3. 操作加入队列 (加入队列后系统调起执行任务) 操作会在新的线程中
[queue addOperation:operation];

// 4. 任务完成监听回调
operation.completionBlock = ^{

[[NSOperationQueue mainQueue] addOperationWithBlock:^{

}];
NSLog(@"NSInvocationOperation 任务执行完成");
};

// 第三步也可以直接我们手动调起操作直接执行,这种情况下任务默认是在当前线程执行不会开启新线程。但是与第三步不能同时使用否则崩溃
// [operation start];
}

- (void)executeOperation:(id)object {
NSLog(@" NSInvocationOperation 执行任务---%@:%@",object,[NSThread currentThread]);
}
1
2
3
// 执行结果打印
... CCBlogCode[24149:4544608] NSInvocationOperation 执行任务---zeroccOperation:<NSThread: 0x600003b24680>{number = 3, name = (null)}
... CCBlogCode[24149:4544609] NSInvocationOperation 任务执行完成

NSBlockOperation 实现示例

NSBlockOperation 创建的 operation 操作,可以通过调用 addExecutionBlock:(void (^)(void))block 方法往操作中添加多个任务,并且具备开启新线程的能力;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)BlockOperation {
// 1. 创建操作 NSBlockOperation 并往操作中添加任务
// 任务1
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1---%@",[NSThread currentThread]);
}];

[operation cancel];
// 任务2加入到操作中
[operation addExecutionBlock:^{
NSLog(@"任务2---%@",[NSThread currentThread]);
}];

// 2. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 3. 操作添加到队列
[queue addOperation:operation];

// 4. 任务完成监听回调
operation.completionBlock = ^{
NSLog(@"NSBlockOperation 任务执行完成");
};
}
1
2
3
4
// 执行结果打印
... CCBlogCode[24203:4547036] 任务1---<NSThread: 0x600000928780>{number = 4, name = (null)}
... CCBlogCode[24203:4547037] 任务2---<NSThread: 0x60000092c5c0>{number = 5, name = (null)}
... CCBlogCode[24203:4547037] NSBlockOperation 任务执行完成

NSOperationQueue 队列常用姿势

队列的类型

主队列:添加到主队列中的操作都会放到主线程中执行。主队列的获取方式:

1
NSOperationQueue *queue = [NSOperationQueue mainQueue];

非主队列:添加到非主队列的操作都会放到子线程中并发执行,等同于并发队列。并行队列创建方式:

1
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

队列中添加操作 Operation

往队列中添加操作的两种方式:

1
2
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block

示例demo:

1
2
3
4
5
6
7
// 方式一
[queue addOperation:operation];

// 方式二
[queue addOperationWithBlock:^{
// 执行任务
}];

队列设置最大并发数

通过设置队列 maxConcurrentOperationCount 这个属性来控制一个特定队列中可以有多少个操作同时参与并发执行。设置为1时,是不是就是串行队列呢?(本人认为不是)。maxConcurrentOperationCount = 1 -> serial queue。这里是虽然是队列的属性,但是控制的不是并发线程的数量,控制的是Operation操作数量,他控制的只是 NSOperation 对象调用多少个操作能同时执行的情况。

先理理并发数这个概念,并发执行过程不一定就开启了新的线程,这里涉及系统线程池对线程的管理,但是如果是串行队列是一定不会开启新线程的。那么看看如下 demo 执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)OperationQueueMaxConcurrentOperationCount{

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
// 任务1加入到操作中
NSLog(@"任务1---%@",[NSThread currentThread]);
}];
// 任务2加入到操作中
[operation1 addExecutionBlock:^{
NSLog(@"任务2---%@",[NSThread currentThread]);
}];
// 任务3加入到操作中
[operation1 addExecutionBlock:^{
NSLog(@"任务3---%@",[NSThread currentThread]);
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{

NSLog(@"operation2---%@",[NSThread currentThread]);
}];

NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{

NSLog(@"operation3---%@",[NSThread currentThread]);
}];
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
}
1
2
3
4
5
6
7
8
// 针对任务分析,一个操作 operation1 中添加多个执行任务,执行的具体任务代码是有开辟新线程的,异步执行
// 针对多个操作分析,operation2 和 operation3 的执行也不是在同一个线程下,也就是说也开辟了新线程,那么结论就是 maxConcurrentOperationCount = 1 情况一定不是串行队列
... CCBlogCode[29007:4865646] 任务2---<NSThread: 0x6000013884c0>{number = 4, name = (null)}
... CCBlogCode[29007:4865645] 任务3---<NSThread: 0x60000138d000>{number = 5, name = (null)}
... CCBlogCode[29007:4865647] 任务1---<NSThread: 0x60000138c840>{number = 3, name = (null)}
... CCBlogCode[29007:4865645] operation2---<NSThread: 0x60000138d000>{number = 5, name = (null)}
... CCBlogCode[29007:4865647] operation3---<NSThread: 0x60000138c840>{number = 3, name = (null)}

队列管理操作 operation 的执行状态

1
2
3
4
5
6
7
// 1. 队列取消所有的操作
[queue cancelAllOperations];
// 2. 队列暂停所有的操作
[queue setSuspended:YES];
// 3. 队列恢复所有的操作
[queue setSuspended:NO];

线程间通信和操作依赖设置

线程间通信:子线程中操作执行完毕回到主线程做相关数据同步或者UI相关;只需在执行任务完成的相应地方获取主队列,并往主队列中添加操作;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 线程间通信 demo
- (void)OperationBackMainThread {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
// 执行耗时操作
[NSThread sleepForTimeInterval:3];
NSLog(@"任务1---%@",[NSThread currentThread]);

// 获取主队列并添加操作
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 刷新 UI 等
NSLog(@"刷新 UI---%@",[NSThread currentThread]);
}];
}];
}
1
2
3
// 线程间通信执行结果打印
... CCBlogCode[29820:4891621] 任务1---<NSThread: 0x600000c523c0>{number = 3, name = (null)}
... CCBlogCode[29820:4891557] 刷新 UI---<NSThread: 0x600000c0a840>{number = 1, name = main}

操作依赖设置:NSOperation 子类实现多线程多个操作之间可以设置依赖关系,等 A 操作执行完再执行 B 操作这样,GCD 中如果要实现这种场景只用通过复杂的 dispatch_barrier_async 分割任务执行顺序实现或者是通过 dispatch_semaphore 信号量实现;但是绝对不能互相依赖,A依赖B,B又依赖A, 不然的话就死锁了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Operation 依赖设置示例 demo
- (void)OperationAddDependency {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:3];
NSLog(@"operation1---%@",[NSThread currentThread]);
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1];
NSLog(@"operation2---%@",[NSThread currentThread]);
}];

NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation3---%@",[NSThread currentThread]);
}];

// 添加依赖
// 让operation3 依赖于 operation2,operation2 依赖于 operation1;
// 尽管是操作1 和 2 都有延迟,其执行顺序也是先operation1->operation2->operation3
[operation2 addDependency:operation1];
[operation3 addDependency:operation2];

[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
}
1
2
3
4
// 执行结果打印
... CCBlogCode[29460:4881327] operation1---<NSThread: 0x6000003a7d80>{number = 3, name = (null)}
... CCBlogCode[29460:4881260] operation2---<NSThread: 0x6000003ab7c0>{number = 4, name = (null)}
... CCBlogCode[29460:4881327] operation3---<NSThread: 0x6000003a7d80>{number = 3, name = (null)}