开始使用Operation Queue吧  

关于NSOperation

上一篇文章把GCD API大体上整理了一下,其实GCD用起来已经比较简单了,而且能解决我们日常开发中多线程编程中的几乎所有问题,但是仍有一些特性是它无法做到的,比如你想取消GCD队列中的某个操作,或者让操作之间具有依赖。以下是operation对象的一些优势。

  • 取消operations

NSOperation类提供了cancel方法,能够告知已经添加到队列中operation停止执行,但是正在执行的operation无法取消。也可以使用NSOperationQueue类提供的方法cancelAllOperations取消队列中所有的operation。

  • 让operation间具有依赖关系

NSOperation类提供了addDependency方法,让一个operation必须等到它依赖的一个或多个operation执行完成后再开始执行。当然了,也可以多个operation依赖于同一个其他的operation,待依赖的operation执行完后,这些operation还可以并发执行。

  • 提供了一些符合KVO的属性

NSOperation有很多属性适合KVO,下面是几个主要的可以用来检测operation执行状态的key paths。

Key Path 用途
isReady 告知调用方该operation已经准备好去执行
isExecuting 告知调用方该operation是否正在执行任务
isFinished 告知调用方该operation是否已经执行完任务
isCancelled 告知调用方该operation是否已经被取消
  • 可以设置operation的优先级

我们可以给operation设置优先级,让其与同一队列中的其他operation的优先级不同。高优先级别的operation执行完,才会执行低优先级的operation。然而GCD没有提供API能够设置同一个队列中不同任务的优先级,队列中任务的优先级与该队列的优先级一样。

  • 可以复用operation

我们自定义的继承自NSOperation的类,就是普通的NSObject对象,可以在我们的代码中重复使用。这也遵循了软件开发中的DRY(Don’t Repeat Yourself)原则。

  • 可以设置completionBlock

可以给operation配置一个completionBlock,在任务执行完后会执行该block。我们可以在这个block里通知感兴趣的调用方任务已经执行完成。并发的operation对象也可以使用这个block生成最后的KVO通知。该completionBlock即不需要参数,也没有返回值。

NSInvocationOperation和NSBlockOperation

Foundation Framework中已经封装好了2个继承自Operation给我们,NSInvocationOperation用来触发指定对象执行某个selector,NSBlockOperation用面向对象的方式来封装一个或多个block。

先来看一下NSInvocationOperation如何创建,对它的认识就会更加清晰了。

1
2
3
4
5
6
7
8
9
- (NSOperation *)taskWithData:(id)data {
NSInvocationOperation *theOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myTaskMethod:) object:data];
return theOp;
}

- (void)myTaskMethod:(id)data {
// Perform the task

}

NSInvocationOperation能够很方便地让我们在运行时决定方法的调用对象。NSInvocationOperation是一个非并发的operation。在调用start后,会立即执行任务,同时start会阻塞当前线程知道任务完成再返回。该NSInvocationOperation在Swift中是没有的,因为在Swift中不可能到运行时才决定方法的调用者。

再来看一下NSBlockOperation的创建,Objective-C版本。

1
2
3
4
5
6
7
8
9
10
11
12
- (NSOperation *)testBlockOperation {
NSBlockOperation *theOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Beginning operation.\n");

}];
for (int i=1; i<=10; i++) {
[theOp addExecutionBlock:^{
NSLog(@"add block %d.\n", i);

sleep(2);
}];
}
return theOp;
}

Swift版本:

1
2
3
4
5
6
7
8
9
10
11
func blockOperation() -> BlockOperation {
let op = BlockOperation {
print("Beginning operation.\n")
}
for i in 0 ..< 5 {
op.addExecutionBlock {
print("add block \(i).\n")
}
}
return op
}

创建之后还可以使用addExecutionBlock:方法继续追加block。调用该operation的start,它会将这些block提交到默认优先级的并发的dispatch queue中,这些block会并发执行。等所有的block都执行结束后,operation会把它自己标记为已完成。由start方法依然会阻塞当前线程直到所有的block都执行结束后再返回。

自定义Operation对象

NSOperation虽然是抽象的,但是已经做了大部分基础建设工作,比如operation间的依赖控制和KVO通知等等。在此基础上你可以自定义一个继承自NSOperation的类,比如将下载图片的操作封装到一个operation的类中。可以实现并发的operation类也可以实现非并发的operation类,但是非并发的operation类实现起来要简单一些。我们先来看一下要自定义一个operation类需要做的基本步骤。

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
@interface MyNonConcurrentOperation : NSOperation

@property (nonatomic, strong) id myData;

- (id)initWithData: (id)data;

@end

@implementation MyNonConcurrentOperation

- (id)initWithData:(id)data {
if (self = [super init]) {
_myData = data;
}
return self;
}

- (void)main {
@try {
if (![self isCancelled]) {
// 处理真正要做的任务
}
} @catch (NSException *exception) {

}
}

@end
  1. 定义一个自定义的初始化方法。
  2. 重写main方法,实现我们的业务逻辑。在start方法做一些检查判断之后,就会去调用main方法。
  3. 响应cancel事件,可以调用isCancelled方法检测operation是否已经被取消。isCancelled是一个轻量的方法,可以频繁调用,几乎不会有性能影响。在实际任务开始之前可以检查,至少在每次循环的条件中检查一下,在比较容易退出operation的地方检查。

上面在我们实现一个非并发的operation已经足够了,下面再来看一下如何实现一个并发的operation。

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
54
55
56
57
58
59
60
61
62
63
64
65
@interface MyConcurrentOperation() {
BOOL _executing;
BOOL _finished;
}

@end

@implementation MyConcurrentOperation

- (id)init {
if (self = [super init]) {
_executing = NO;
_finished = NO;
}
return self;
}

- (BOOL)isConcurrent {
return YES;
}

- (BOOL)isExecuting {
return _executing;
}

- (BOOL)isFinished {
return _finished;
}

- (void)start {
if ([self isCancelled]) {
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}

[self willChangeValueForKey:@"isExecuting"];
// 开新的线程处理任务
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}

- (void)main {
@try {
// 处理主要的业务逻辑

} @catch (NSException *exception) {

}
}

- (void)completeOperation {
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];

_executing = NO;
_finished = YES;

[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}

@end

Swift版本的实现如下:

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
54
55
class MyConcurrent: Operation {
var _executing: Bool
var _finished: Bool

override init() {
_executing = false
_finished = false
super.init()
}

override var isConcurrent: Bool {
return true
}

override var isExecuting: Bool {
return _executing
}

override var isFinished: Bool {
return _finished
}

override func start() {
if isCancelled {
willChangeValue(forKey: "isFinished")
_finished = true
didChangeValue(forKey: "isFinished")
return
}

willChangeValue(forKey: "isExecuting")
Thread.detachNewThreadSelector(Selector("main"), toTarget: self, with: nil)
_executing = true
didChangeValue(forKey: "isExecuting")
}

override func main() {
do {
try <#throwing expression#>
} catch <#pattern#> {

}
}

private func completeOperation () {
willChangeValue(forKey: "isExecuting")
willChangeValue(forKey: "isFinished")

_executing = false
_finished = true

didChangeValue(forKey: "isExecuting")
didChangeValue(forKey: "isFinished")
}
}
  1. 重写start方法,这是必须的。手动启动operation,需要调用start方法。在start中可以配置线程,或其他执行任务之前要做的准备工作。任何时候都不要在start中调用super
  2. 重写main方法,这是可选的。你也可以把要做的任务放在start中处理,但是为了代码能够清晰明了,最好在start做一些配置工作,在main中做真正的任务。
  3. 重写isExecutingisFinished,这是必须的。因为operation是并发的,所以只有你才能准确知道任务是否正在进行,是否已经完成。还必须在合适的时候产生这两个key paths的KVO通知,以便调用者能得到通知。
  4. 重写isConcurrent,这是必须的。该方法必须返回YES

关于NSOperationQueue

添加Operations到Operation Queue中

上面我们说到的启动一个operation都是用的start方法,但是start方法会阻塞当前线程。如果想要在新线程中启动operation,我们需要开线程运行operation的start方法。

1
2
[NSThread detachNewThreadSelector:@selector(start)
toTarget:anOp withObject:nil];

还可以将operation添加到operation queue中启动operation运行,operation一旦被添加到operation queue中随时都可能运行,而且不会阻塞当前线程。该operation queue是NSOperationQueue的实例对象。NSOperationQueue提供了多个添加operation的方法的。

1
2
3
4
5
6
7
8
// 添加单个operation到queue中
[aQueue addOperation:anOp];
// 添加一组operation到queue中
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO];
// 直接以Block块的方式添加
[aQueue addOperationWithBlock:^{

}];

虽然我们可以在应用中创建很多个operation queue,但是在给定的时间能运行多少个operation queue是由当前的系统情况决定的,所以并不会创建的operation queue越多,能够运行也越多。

配置Operation Queue

获取NSOperationQueue实例的方式:

1
2
3
4
5
6
// 可以直接用init创建一个queue
NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
// 获取主队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
// 获取当前对类
NSOperationQueue *currentQueue = [NSOperationQueue currentQueue];

还可以通过maxConcurrentOperationCount设置队列同时能并发的最大operation的个数,如果设置为1,那同时就只能有一个任务在执行。

通过cancelAllOperations可以取消队列中剩余未开始执行的任务。

通过waitUntilAllOperationsAreFinished阻塞当前线程,等待所有的operation执行结束。

参考资料

【1】 苹果官方文档Concurrency and Application Design

【2】 苹果官方文档NSOperation

【2】 《Effective Objective-C 2.0》(英文版)

本文作者:意林
本文链接:http://shinancao.cn/2019/06/18/iOS-NSOperation/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!