【iOS】多线程系列四 - 操作和队列

NSOperationNSOperationQueue配合使用也能实现多线程编程。

一、操作

NSOperation是对GCD封装的抽象类,使用起来更加面向对象。但是它不具备封装操作的能力,必须使用它的子类才可以。

系统为我们提供了两个NSOperation的子类:

  • NSInvocationOperation:把任务放到了函数中。

  • NSBlockOperation:把任务放到block块中。

我们也可以自定义继承自NSOperation的子类实现内部相应的方法。

1.1. 实现步骤

NSOperationNSOperationQueue实现多线程的具体步骤:

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

1.2. 封装操作

1.2.1. 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
25
26
27
28
29
30
- (void)invocationOperation {
// 封装操作对象
NSInvocationOperation *operationA = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskA) object:nil];
NSInvocationOperation *operationB = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskB) object:nil];
NSInvocationOperation *operationC = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskC) object:nil];

// 执行操作
[operationA start];
[operationB start];
[operationC start];
}

- (void)taskA {
NSLog(@"taskA--%@", [NSThread currentThread]);
}

- (void)taskB {
NSLog(@"taskB--%@", [NSThread currentThread]);
}

- (void)taskC {
NSLog(@"taskC--%@", [NSThread currentThread]);
}

/*
输出:
taskA--<NSThread: 0x600002b91200>{number = 1, name = main}
taskB--<NSThread: 0x600002b91200>{number = 1, name = main}
taskC--<NSThread: 0x600002b91200>{number = 1, name = main}
*/

1.2.2. NSBlockOperation

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 {
// 封装操作对象
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskB--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskC--%@", [NSThread currentThread]);
}];

// 执行操作
[operationA start];
[operationB start];
[operationC start];
}
/*
输出:
taskA--<NSThread: 0x600000b1fb80>{number = 1, name = main}
taskB--<NSThread: 0x600000b1fb80>{number = 1, name = main}
taskC--<NSThread: 0x600000b1fb80>{number = 1, name = main}
*/

NSBlockOperation可以通过addExecutionBlock:方法增加任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[operationB addExecutionBlock:^{
NSLog(@"taskD--%@", [NSThread currentThread]);
}];
[operationB addExecutionBlock:^{
NSLog(@"taskE--%@", [NSThread currentThread]);
}];
/*
输出:
taskA--<NSThread: 0x600001bd0c00>{number = 1, name = main}
taskE--<NSThread: 0x600001b88ec0>{number = 4, name = (null)}
taskD--<NSThread: 0x600001ba4100>{number = 5, name = (null)}
taskB--<NSThread: 0x600001bd0c00>{number = 1, name = main}
taskC--<NSThread: 0x600001bd0c00>{number = 1, name = main}
*/

当一个操作中的任务数量大于1的时候,就会开启子线程和主线程一起执行任务。

1.2.3. 自定义Operation

自定义Operation时,自定义类只有通过重写main方法封装任务。因为队列调用addOperation:执行任务时,会先调用操作的start方法,而start方法会调用main方法。

示例代码:

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
// DBOperation.h
@interface DBOperation : NSOperation

@end

// DBOperation.m
@implementation DBOperation

// 重写main方法(不需要调用super)封装任务,会自动执行
- (void)main {
NSLog(@"main--%@", [NSThread currentThread]);
for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"a--%ld--%@", i, [NSThread currentThread]);
}
// 官方建议:自定义操作时,每执行完一个耗时操作就判断一下操作是否被取消,如果被取消就直接返回
if (self.isCancelled) {
return;
}

for (NSInteger i = 0; i < 1000; i++) {
NSLog(@"b--%ld--%@", i, [NSThread currentThread]);
}
}

@end

// 使用
- (void)customOperation {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
DBOperation *operation = [[DBOperation alloc] init];
[queue addOperation:operation];
}

二、队列

操作队列分为自定义队列和主队列。

  • 自定义队列:[[NSOperationQueue alloc] init],并发队列(但是可以通过控制并发数让其成为串行队列)
  • 主队列:[NSOperationQueue mainQueue],串行队列,和主线程相关。

2.1. 基本使用

示例代码(NSInvocationOperation):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)invocationOperationWithQueue {
// 1. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2. 封装操作
NSInvocationOperation *operationA = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskA) object:nil];
NSInvocationOperation *operationB = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskB) object:nil];
NSInvocationOperation *operationC = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskC) object:nil];
// 3. 操作添加到队列
[queue addOperation:operationA];
[queue addOperation:operationB];
[queue addOperation:operationC];
}
/*
输出:
taskC--<NSThread: 0x6000030c5480>{number = 6, name = (null)}
taskA--<NSThread: 0x6000030f0e80>{number = 5, name = (null)}
taskB--<NSThread: 0x6000030e1f00>{number = 4, name = (null)}
*/

结论:操作不需要手动开启,队列会自动开启操作并执行任务。并且任务是在子线程中执行的。

示例代码():

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
- (void)blockOperationWithQueue {
// 1. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2. 封装操作
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskB--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskC--%@", [NSThread currentThread]);
}];

[operationB addExecutionBlock:^{
NSLog(@"taskD--%@", [NSThread currentThread]);
}];
[operationB addExecutionBlock:^{
NSLog(@"taskE--%@", [NSThread currentThread]);
}];
// 3. 操作添加到队列
[queue addOperation:operationA];
[queue addOperation:operationB];
[queue addOperation:operationC];
}
/*
输出:
taskC--<NSThread: 0x600000633800>{number = 4, name = (null)}
taskB--<NSThread: 0x60000060c840>{number = 6, name = (null)}
taskA--<NSThread: 0x600000636e80>{number = 5, name = (null)}
taskD--<NSThread: 0x60000060c700>{number = 7, name = (null)}
taskE--<NSThread: 0x60000060c840>{number = 6, name = (null)}
*/

结论和NSInvocationOperation一样。

队列添加操作还有一种简洁的写法:

1
2
3
4
5
6
7
8
9
[queue addOperationWithBlock:^{
NSLog(@"taskC--%@", [NSThread currentThread]);
}];

// 等价于
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskC--%@", [NSThread currentThread]);
}];
[queue addOperation:operationC];

2.2. 最大并发数

队列属性maxConcurrentOperationCount可以设置最大并发数(同一时间有多少线程在执行)。

默认值是-1,代表不受限制。

1
static const NSInteger NSOperationQueueDefaultMaxConcurrentOperationCount = -1;

如果设置为0,代表不执行任务。

设置最大并发数对于任务数量大于1的操作是无效的(针对的是追加任务)。当操作中任务数量大于1时,会开启多条子线程和当前线程一起工作。

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
- (void)blockOperationWithQueue {
// 1. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 设置最大并发数
queue.maxConcurrentOperationCount = 1;

// 2. 封装操作
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskB--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskC--%@", [NSThread currentThread]);
}];

[operationB addExecutionBlock:^{
NSLog(@"taskD--%@", [NSThread currentThread]);
}];
[operationB addExecutionBlock:^{
NSLog(@"taskE--%@", [NSThread currentThread]);
}];
// 3. 操作添加到队列
[queue addOperation:operationA];
[queue addOperation:operationB];
[queue addOperation:operationC];
}
/*
输出:
taskA--<NSThread: 0x600000a23500>{number = 3, name = (null)}
taskE--<NSThread: 0x600000a3e0c0>{number = 4, name = (null)}
taskB--<NSThread: 0x600000a23500>{number = 3, name = (null)}
taskD--<NSThread: 0x600000a30840>{number = 5, name = (null)}
taskC--<NSThread: 0x600000a30840>{number = 5, name = (null)}
*/

建议开发中使用NSBlockOperation或者自定义操作,因为在Swift中不能使用NSInvocationOperation

2.3. 暂停和取消

队列可以设置暂停、继续、取消。

示例代码:

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
48
49
50
51
52
53
// 开始
- (IBAction)startTasks {
[self blockOperationWithQueue];
}

// 暂停
- (IBAction)suspendedTasks {
self.queue.suspended = YES;
}

// 继续
- (IBAction)resumeTasks {
self.queue.suspended = NO;
}

// 取消
- (IBAction)cancelTasks {
[self.queue cancelAllOperations];
}

- (void)blockOperationWithQueue {
// 1. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 设置最大并发数
queue.maxConcurrentOperationCount = 1;

// 2. 封装操作
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
for (NSInteger i = 0; i < 10000; i++) {
// 模拟耗时任务
}
NSLog(@"taskA--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
for (NSInteger i = 0; i < 10000; i++) {
// 模拟耗时任务
}
NSLog(@"taskB--%@", [NSThread currentThread]);
}];
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
for (NSInteger i = 0; i < 10000; i++) {
// 模拟耗时任务
}
NSLog(@"taskC--%@", [NSThread currentThread]);
}];

// 3. 操作添加到队列
[queue addOperation:operationA];
[queue addOperation:operationB];
[queue addOperation:operationC];

self.queue = queue;
}

注意:只能暂停/取消还未开始执行的操作,正在执行任务的操作不可分割,必须执行完毕。

2.4. 依赖和监听

2.4.1. 依赖

操作依赖非常简单,只需要设置addDependency:即可。
示例代码:

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
- (void)operationDependency {
// 1. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2. 封装操作
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskA-%@", [NSThread currentThread]);
}];
NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskB-%@", [NSThread currentThread]);
}];
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"taskC-%@", [NSThread currentThread]);
}];

// 3. 设置依赖(必须在被添加到队列前设置依赖)
[operationA addDependency:operationB];
[operationB addDependency:operationC];

// 4. 操作添加到队列
[queue addOperation:operationA];
[queue addOperation:operationB];
[queue addOperation:operationC];
}
/*
输出:
taskC-<NSThread: 0x600003b41bc0>{number = 5, name = (null)}
taskB-<NSThread: 0x600003b41bc0>{number = 5, name = (null)}
taskA-<NSThread: 0x600003b41bc0>{number = 5, name = (null)}
*/

依赖是可以跨队列的,比如operationA在队列queueA中,operationB在队列queueB中,可以设置[operationA addDependency:operationB],等待operationB执行完成后再执行operationA

注意:必须在被添加到队列前设置依赖。不能设置循环依赖,否则产生循环的任务不会执行。

2.4.2. 监听

执行操作的完成回调函数completionBlock就可以监听到操作任务是否执行完毕。

1
2
3
operationC.completionBlock = ^{
NSLog(@"operationC任务执行完毕");
};

2.5. 线程间通信

使用[NSOperationQueue mainQueue]就可以获取到主队列,addOperationWithBlock添加的任务是在主线程中执行的。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)downloadImage {
// 1. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2. 封装操作
NSBlockOperation *downloadOperation = [NSBlockOperation blockOperationWithBlock:^{
NSString *imgURLStr = @"https://image.baidu.com/search/detail?ct=503316480&z=9&tn=baiduimagedetail&ipn=d&word=5k%E9%AB%98%E6%B8%85%E5%A4%A7%E5%9B%BE&step_word=&ie=utf-8&in=&cl=2&lm=-1&st=-1&hd=0&latest=0&copyright=0&cs=3388459890,4007020485&os=1361240164,2722658368&simid=0,0&pn=0&rn=1&di=69520&ln=787&fr=&fmq=1605533629706_R&fm=rs4&ic=0&s=undefined&se=&sme=&tab=0&width=0&height=0&face=undefined&is=0,0&istype=0&ist=&jit=&bdtype=0&spn=0&pi=0&gsm=0&hs=2&oriquery=%E5%A4%A7%E5%9B%BE&objurl=http%3A%2F%2Fimg.jk51.com%2Fimg_jk51%2F196318182.jpeg&rpstart=0&rpnum=0&adpicid=0&force=undefined";
NSURL *imgURL = [NSURL URLWithString:imgURLStr];
NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
UIImage *img = [UIImage imageWithData:imgData];
// 4. 在主线程中刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imgView.image = img;
}];
}];

// 3. 操作添加到队列
[queue addOperation:downloadOperation];
}

三、GCD和NSOperation的对比

  1. GCD是纯C语言的API,而操作队列则是OC对象。
  2. 在GCD中,任务用块(block)来表示,而块是个轻量级的数据结构。相反操作队列中的NSOperation则是个更加轻量级的OC对象。
  3. NSOperation可以方便的调用cancel方法来取消某个操作,而GCD中的任务是无法被取消的。
  4. NSOperation可以方便的指定操作间的依赖关系。
  5. NSOperation可以通过KVO提供对NSOperation对象的精细控制(如监听当前操作是否被取消或是否已经完成等)。
  6. NSOperation可以方便的指定操作优先级。
  7. 通过自定义NSOperation的子类可以实现操作重用(重写main方法)。
  8. 具体使用GCD还是NSOperation应看具体的情况(一般情况下,复杂业务使用NSOperation,简单业务使用GCD)。