NSOperation 就是在 GCD 的基础上封装了一层,它也有任务和队列的概念,比 GCD 多了的就是面向对象的概念吧。

扯个蛋先

配合使用 NSOperation 和 NSOperationQueue 也能实现多线程编程,其中,NSOperation 就相当于 GCD 的任务,NSOperationQueue 就相当于 GCD 中的队列。通过配合使用 NSOperation 和 NSOperationQueue 实现多线程编程的步骤和使用 GCD 也很类似:

  1. 先将需要执行的操作封装到一个 NSOperation 对象中;
  2. 然后将 NSOperation 对象添加到 NSOperationQueue 中;
  3. 系统会自动将 NSOperationQueue 中的 NSOperation 取出来;
  4. 将取出的 NSOperation 封装的操作放到一条新线程中执行。

注意一点:NSOperation 是一个抽象类,As we all know,并不能够直接实例化使用,因此它是不具备封装任务的能力,所以必须得使用它的子类。Apple 已经给提供了两个 NSOperation 供我们使用:NSInvocationOperation 和 NSBlockOperation,但是不怎么常用,比较常用的是我们自己自定义子类继承 NSOperation,实现内部相应的方法。

不过,我们还是先看看 NSInvocationOperation 和 NSBlockOperation 这两个对象的使用吧。OK, talk is cheap, show me the code~

NSInvocationOperation

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad {
[super viewDidLoad];
// 创建NSInvocationOperation对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(getupHi:) object:@"angelen"];
// 调用 start 方法开始执行任务,一旦执行任务,就会调用 target 的 sel 方法
[operation start];
}
- (void)getupHi:(NSString *)name {
NSLog(@"%@,起来嗨~,%@", name, [NSThread currentThread]);
}

运行结果:

1
angelen,起来嗨~,<NSThread: 0x7faec8f03150>{number = 1, name = main}

从运行结果我们可以看到,这个任务是在主线程(UI 线程)中执行的,为啥跟说好的多线程不一样呢?这是因为在默认的情况下,调用了 start 方法之后并不会开启一条新线程去执行任务,而是会在当前的线程同步执行任务。只有当这个这个任务(operation)放到一个 NSOperationQueue 中才会开启新的线程,异步执行任务(这个后面再说)。

NSBlockOperation

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)viewDidLoad {
[super viewDidLoad];
// 创建 NSBlockOperation 对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1, %@", [NSThread currentThread]);
}];
// (可选)可以通过 addExecutionBlock: 方法添加更多的任务(操作)
[operation addExecutionBlock:^{
NSLog(@"2, %@", [NSThread currentThread]);
}];
[operation addExecutionBlock:^{
NSLog(@"3, %@", [NSThread currentThread]);
}];
[operation addExecutionBlock:^{
NSLog(@"3, %@", [NSThread currentThread]);
}];
// 调用 start 方法开始执行任务
[operation start];
}

运行结果:

1
2
3
4
1, <NSThread: 0x7fe6ab503750>{number = 1, name = main} // 第一个任务是在当前线程执行的
3, <NSThread: 0x7fe6ab70f890>{number = 3, name = (null)} // 任务 > 1 时,会开新线程
2, <NSThread: 0x7fe6ab60a7c0>{number = 2, name = (null)}
3, <NSThread: 0x7fe6ab60dd10>{number = 4, name = (null)}

从运行结果我们可以发现,NSBlockOperation 封装的第一个任务是同步执行的,当 NSBlockOperation 封装任务数 > 1 的时候,就会执行异步操作。

注意:addExecutionBlock: 方法只能在 start 方法之前添加,否则会报错:blocks cannot be added after the operation has started executing or finished。

NSOperationQueue

NSOperationQueue 就相当于 GCD 中的队列,在 GCD 中,队列分为两大种(细分 4 种):

  1. 并发队列(自己创建的、全局队列)
  2. 串行队列(自己创建的、主队列)

对应来说,NSOperationQueue 也有两种队列:

  1. 主队列(通过 [NSOperationQueue mainQueue] 取得,凡是添加到主队列的任务(NSOperation),都会在主线程中执行)
  2. 非主队列(其他队列,通过 [[NSOperationQueue alloc] init] 创建,含有并行和串行的功能,凡是添加到非主队列的任务(NSOperation),都会在子线程中执行)

通过上面 NSInvocationOperation 和 NSBlockOperation 的使用我们可以发现,NSOperation 可以调用 start 方法来执行任务,但默认是没有开启新线程,是同步执行的。但是如果将 NSOperation 添加到 NSOperationQueue(操作队列)中,系统就会自动异步执行 NSOperation 中的任务。

简单使用:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (void)viewDidLoad {
[super viewDidLoad];
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建任务 1的(NSInvocationOperation)
NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
// 创建任务 2(NSInvocationOperation)
NSInvocationOperation *operation2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
// 创建任务 3(NSBlockOperation)
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download3, %@", [NSThread currentThread]);
}];
// 创建任务 4
[operation3 addExecutionBlock:^{
NSLog(@"download4, %@", [NSThread currentThread]);
}];
// 创建任务 5
[operation3 addExecutionBlock:^{
NSLog(@"download5, %@", [NSThread currentThread]);
}];
[queue addOperation:operation1]; // 会自动调用 operation1 的 start 方法
[queue addOperation:operation2];
[queue addOperation:operation3];
// 创建任务 6 并且添加到队列中
[queue addOperationWithBlock:^{
NSLog(@"download6, %@", [NSThread currentThread]);
}];
// 或者还可以批量将任务添加到队列中
// [queue addOperations:@[operation1, operation2, operation3] waitUntilFinished:NO];
}
- (void)download1 {
NSLog(@"download1, %@", [NSThread currentThread]);
}
- (void)download2 {
NSLog(@"download2, %@", [NSThread currentThread]);
}

运行结果:

1
2
3
4
5
6
download6, <NSThread: 0x7faa52448e60>{number = 3, name = (null)}
download3, <NSThread: 0x7faa52568e60>{number = 4, name = (null)}
download1, <NSThread: 0x7faa524494b0>{number = 5, name = (null)}
download5, <NSThread: 0x7faa52746810>{number = 7, name = (null)}
download2, <NSThread: 0x7faa524496a0>{number = 6, name = (null)}
download4, <NSThread: 0x7faa5255c370>{number = 2, name = (null)}

从结果可以看到,添加到 NSOperationQueue 的任务,会自动开启新线程异步执行。

从上面我们已经将 NSInvocationOperation 和 NSBlockOperation 这两种任务添加到队列中了,如果有时候我们想自己定制一些异步操作(任务),那么我们可以继承 NSOperation,实现 main 方法,在 main 方法做自己所需的任务,然后把这个任务添加到队列中即可(当然我们还可以实现其他的方法,定制好这个 custom operation)。

1
2
3
4
5
6
7
8
9
10
11
12
#import "ZLImageDownloadOperation.h"
@implementation ZLImageDownloadOperation
/**
* 在 main 方法执行任务,当把这个任务添加到 queue 中,这个任务就会自动调用 start 方法,执行 main 函数里的操作
*/
- (void)main {
NSLog(@"Just download images -> %@", [NSThread currentThread]);
}
@end

沿用上面 demo 的 queue:

1
2
ZLImageDownloadOperation *imageDownloadOperation = [[ZLImageDownloadOperation alloc] init];
[queue addOperation:imageDownloadOperation];

运行效果:

1
2
3
4
5
6
7
download6, <NSThread: 0x7f8aa2731210>{number = 3, name = (null)}
download1, <NSThread: 0x7f8aa24013c0>{number = 2, name = (null)}
Just download images -> <NSThread: 0x7f8aa264e880>{number = 6, name = (null)}
download3, <NSThread: 0x7f8aa2507d80>{number = 5, name = (null)}
download2, <NSThread: 0x7f8aa2731170>{number = 4, name = (null)}
download4, <NSThread: 0x7f8aa2731ae0>{number = 7, name = (null)}
download5, <NSThread: 0x7f8aa244fbb0>{number = 8, name = (null)}

在上面我们说过 NSOperationQueue 其中一种类型 —— 非主队列既能并发执行任务,也能串行执行任务,这是通过设置 NSOperationQueue 的一个属性 maxConcurrentOperationCount(最大并发数:同时执行的任务数,比如,同时开 3 个线程执行 3 个任务,并发数就是 3)来决定的。

1
2
3
4
5
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 设置最大并发数
queue.maxConcurrentOperationCount = 2;
// 创建任务,并添加任务到队列中
  • 如果设置 maxConcurrentOperationCount = 1 ,被添加的任务就会串行执行,但是还是会开线程,这个队列就是串行队列。
  • 如果设置 maxConcurrentOperationCount > 1,这个队列就是并发队列。
  • 如果设置 maxConcurrentOperationCount = 0,不好意思,这些任务无法执行。
  • 如果设置 maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount = -1,默认值。The default maximum number of operations is determined dynamically by the NSOperationQueue object based on current system conditions.(官方解释不翻译了,都看得懂)

除了可以设置队列的最大并发数,还可以对队列做一些 取消、暂停(挂起)、恢复 的操作。

1
2
3
4
5
// 暂停(挂起)和恢复队列 -> 暂停和继续执行任务
@property (getter=isSuspended) BOOL suspended; // YES 代表暂停队列,NO 代表恢复队列
// 取消队列的所有操作
- (void)cancelAllOperations;

一个简单例子演示队列的暂停(挂起)和恢复的操作:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import "ViewController.h"
@interface ViewController ()
@property (strong, nonatomic) NSOperationQueue *queue;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 1; // 设置最大并发数为 1 -> 队列为串行队列
[self.queue addOperationWithBlock:^{
NSLog(@"执行任务 1");
[NSThread sleepForTimeInterval:2.0]; // 用来让这个任务执行时间长久一点
}];
[self.queue addOperationWithBlock:^{
NSLog(@"执行任务 2");
[NSThread sleepForTimeInterval:2.0]; // 用来让这个任务执行时间长久一点
}];
[self.queue addOperationWithBlock:^{
NSLog(@"执行任务 3");
[NSThread sleepForTimeInterval:2.0]; // 用来让这个任务执行时间长久一点
}];
NSLog(@"I'm logging...");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.queue.isSuspended) { // 队列是否被挂起(暂停)
// 恢复队列,继续执行任务
self.queue.suspended = NO;
} else {
// 挂起队列,暂停执行任务
self.queue.suspended = YES;
}
}
@end

程序运行后:

1
2
I'm logging...
执行任务 1

当我们点击屏幕的时候,可以切换队列的挂起状态:

1
2
执行任务 2
执行任务 3

很多时候,我们如果想让用户滑动 UITableView 的时候感觉很流畅,之前的做法就是可以在当 UITableView 准备开始滑动的时候,暂停执行任务,当 UITableView 准备结束滚动的时候,继续执行任务。现在的做法 -> 暂时不知道,TODO。

注意:以上例子中,如果任务 1 是一个耗时操作(比如打印 5000 次),当打印到 2000 次的时候,我们点击屏幕,把队列挂起,此时,任务 1 还是会执行完的,它只会挂起任务 2 和任务 3 的。

另外,对于队列的 cancelAllOperations 方法就是取消所有的操作(任务)的,其实就是调用了所有 operation 的 cancel 方法,但是,如果一个任务正在执行的过程中,是无法取消掉的。

还有一点,也是 Apple 官方建议的,就是在自定义任务(继承 NSOperation 实现 main 方法)做几个比较耗时的操作,建议手动在几个耗时操作之间加入:如果 isCancelled = YES,则直接不执行下面的耗时操作,人为的控制外面队列的 cancel 操作。

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
32
33
#import "ZLImageDownloadOperation.h"
@implementation ZLImageDownloadOperation
/**
* 在 main 方法执行任务,当把这个任务添加到 queue 中,这个任务就会自动调用 start 方法,执行 main 函数里的操作
*/
- (void)main {
// 耗时操作 1
for (NSInteger i = 0; i < 10000; i++) {
NSLog(@"download1 - %zd", i);
}
if (self.isCancelled) {
return;
}
// 耗时操作 2
for (NSInteger i = 0; i < 10000; i++) {
NSLog(@"download2 - %zd", i);
}
if (self.isCancelled) {
return;
}
// 耗时操作 3
for (NSInteger i = 0; i < 10000; i++) {
NSLog(@"download3 - %zd", i);
}
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "ViewController.h"
#import "ZLImageDownloadOperation.h"
@interface ViewController ()
@property (strong, nonatomic) NSOperationQueue *queue;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.queue = [[NSOperationQueue alloc] init];
[self.queue addOperation:[[ZLImageDownloadOperation alloc] init]];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.queue cancelAllOperations];
}
@end

运行程序,当打印耗时操作 1 的时候,点击屏幕取消所有的操作,程序会打印完耗时操作 1,取消掉往后的耗时操作。

重写 main 方法需要注意的几点:

  • 经常通过 isCancelled 方法来检测操作是否被取消,以减少对资源的损耗,对取消做出响应。

很多时候我们有这样的需求:执行完任务 1,然后才能去执行任务 2,这个时候我们就可以去设置 NSOperation 之间的 依赖 以保证执行顺序,在 NSOperation 有这样的方法:

1
2
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

比如说如果一定要操作 A 执行完后,才能执行操作 B,就可以这样写:

1
[operationB addDependency:operationA];

下面实战一下:

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
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op2");
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op3");
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op4");
}];
[op3 addDependency:op4]; // 执行完 op4 后才会执行 op3
[op1 addDependency:op2]; // 执行完 op2 后才会执行 op1
[queue addOperations:@[op1, op2, op3, op4] waitUntilFinished:YES];
// 如果 waitUntilFinished 为 YES,则当前线程会阻塞,直到所有任务执行完毕才会执行下面的代码;
// 如果 waitUntilFinished 为 NO,则 the operations are added to the queue and control returns immediately to the caller,即任务添加到队列之后,就继续执行下面的代码
NSLog(@"-> waitUntilFinished");
}

通过加了两个依赖,能够保证执行的顺序是:op4 -> op3 和 op2 -> op1

1
2
3
4
5
op4
op2
op3
op1
-> waitUntilFinished

注意:两个操作之间不能够互相依赖,即下面这样写是错误的:

1
2
[op2 addDependency:op1];
[op1 addDependency:op2];

这样会导致 op1 和 op2 都无法执行,如果添加任务到队列的方式是通过

1
[queue addOperations:@[op1, op2, op3, op4] waitUntilFinished:YES];

来添加的话,则会阻塞当前线程,科科。

除了在同一个 queue 的任务之间可以创建依赖关系,不同 queue 的任务之间也可以创建依赖关系:

不同队列的任务之间创建依赖关系

就是说任务的执行顺序是:11 -> 23 -> 12 -> 13(即:13 依赖于 12 的完成,12 依赖于 23 的完成,23 依赖于 11 的完成)。

对于 NSOperation 还有一点就可以可以 监听 这个任务的执行完毕。

1
2
3
4
5
6
7
8
9
10
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 使用:
op2.completionBlock = ^{
NSLog(@"op2 compelete..."); // 不一定和执行 op2 是同一个线程,但是还是子线程
};
// 或者:
[op1 setCompletionBlock:^{
NSLog(@"op1 compelete...");
}];

线程间通信

在学习 GCD 的时候,我们可以通过在子线程做下载图片这个耗时操作,当图片下载完成时,要回到主线程,进行图片的显示,在 NSOperationQueue 中也是类似:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
// 在子线程这里执行耗时的操作
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://s3.amazonaws.com/sitebuilderreport-assets/stock_photos/files/000/028/295/small/tumblr_oeyy3mHBQc1v4yqceo1_500.jpg?1476396054"]];
UIImage *image = [UIImage imageWithData:data];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 在这里回到主线程(UI 线程),执行 UI 刷新操作
self.imageView.image = image;
}];
}];
}