NSOperation

前几天整理了一下关于多线程GCD的相关内容, 苹果还有一种高级的多线程处理方式是使用NSOperation, 它拥有着和GCD同样的特点即不需要手动管理线程的声明周期, 而是让开发者把重点关注在自己的方法处理上, 并且NSOperation还有着可以手动控制开始, 取消操作等优点。NSOperation完全是Objective-C对象的形式, 个人感觉NSOperation应该是苹果主推的多线程管理框架, = = 但基本上面试问GCD的还是多一点。NSOperation官方文档

NSOperation

Because the NSOperation class is an abstract class, you do not use it directly but instead subclass or use one of the system-defined subclasses (NSInvocationOperation or NSBlockOperation) to perform the actual task.

苹果的官方原话, 意思是说NSOperation是一个抽象类, 不能直接被使用, 你可以使用它的子类NSInvocationOperationNSBlockOperation

基本使用

1
2
3
4
5
6
7
8
9
10
11
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction) object:nil];
[operation start];

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

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

一种是target action的形式, 一种是block回调的形式, 接下来运行一下:

1
2
3
4
5
***** 时间00:47:29 ViewController.m 第30行 *****
<NSThread: 0x2804b5cc0>{number = 1, name = main}

***** 时间00:47:29 ViewController.m 第24行 *****
<NSThread: 0x2804b5cc0>{number = 1, name = main}

可以看到, 在直接使用NSInvocationOperationNSBlockOperation时, 它们都是运行在主线程之上的。

NSBlockOperation

但是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
25
26
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
}];
[blockOperation start];

// Log
***** 时间15:01:34 ViewController.m 79 *****
<NSThread: 0x280ba1100>{number = 6, name = (null)}

***** 时间15:01:34 ViewController.m 76 *****
<NSThread: 0x280bce600>{number = 1, name = main}

***** 时间15:01:34 ViewController.m 82 *****
<NSThread: 0x280ba1100>{number = 6, name = (null)}

***** 时间15:01:34 ViewController.m 85 *****
<NSThread: 0x280bce600>{number = 1, name = main}

所以NSBlockOperation中的任务不一定是运行在主线程之上的。

自定义NSOperation子类

既然NSOperation是一个抽象类, 我们不妨自己实现一个, 先看一下它的方法和属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)start;
- (void)main;
- (void)cancel;
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;
- (void)waitUntilFinished API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

@property (readonly, getter=isCancelled) BOOL cancelled;
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isAsynchronous) BOOL asynchronous API_AVAILABLE(macos(10.8), ios(7.0), watchos(2.0), tvos(9.0));
@property (readonly, getter=isReady) BOOL ready;
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
@property NSOperationQueuePriority queuePriority;
@property (nullable, copy) void (^completionBlock)(void) API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
@property NSQualityOfService qualityOfService API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

属性方面大多数是任务的状态, 如果需要观察状态我们可以用kvo的方式监听一下。方法包含开始取消还有一个main, 根据NSInvocationOperationNSBlockOperation的使用方法来看, 这个main方法很可能是我们需要实现功能的方法, 而NSInvocationOperationNSBlockOperation是以回调的形式把main的实现留给我们。可以去官方文档看一下:

Performs the receiver’s non-concurrent task.

The default implementation of this method does nothing. You should override this method to perform the desired task. In your implementation, do not invoke super. This method will automatically execute within an autorelease pool provided by NSOperation, so you do not need to create your own autorelease pool block in your implementation. If you are implementing a concurrent operation, you are not required to override this method but may do so if you plan to call it from your custom start method.

确实是这样的, main方法默认不做任何事情, 我们需要重写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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@interface GQOperation : NSOperation
@property (nonatomic, strong) dispatch_block_t taskBlock;
@end

@implementation GQOperation

- (instancetype)init {
    self = [super init];
    if (self) {
        [self addObserver:self forKeyPath:@"cancelled" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        [self addObserver:self forKeyPath:@"executing" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        [self addObserver:self forKeyPath:@"finished" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    }
    return self;
}

- (void)main {
    if (_taskBlock != nil) {
        _taskBlock();
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"keyPath = %@, change = %@", keyPath, change);
}

- (void)dealloc {
    NSLog(@"Operation 释放了");
    [self removeObserver:self forKeyPath:@"cancelled"];
    [self removeObserver:self forKeyPath:@"executing"];
    [self removeObserver:self forKeyPath:@"finished"];
}
@end


// 调用方式
NSLog(@"start %@", [NSThread currentThread]);
GQOperation *operation = [[GQOperation alloc] init];
operation.name = @"com.greg.gqoperation";
__weak typeof(operation) weakOperation = operation;
operation.taskBlock = ^{
    __strong typeof(weakOperation) strongOperation = weakOperation;
    [strongOperation cancel];
    if (!strongOperation.isCancelled) {
        NSLog(@"task %@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];
    }
};
operation.completionBlock = ^{
    NSLog(@"operation 执行结束 %@", [NSThread currentThread]);
};
[operation start];
NSLog(@"end %@", [NSThread currentThread]);

打印结果:

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
***** 时间11:41:04 ViewController.m 第22行 *****
start <NSThread: 0x280ab2ec0>{number = 1, name = main}

***** 时间11:41:04 GQOperation.m 第33行 *****
keyPath = executing, change = {
    kind = 1;
    new = 1;
    old = 0;
}

***** 时间11:41:04 GQOperation.m 第33行 *****
keyPath = cancelled, change = {
    kind = 1;
    new = 1;
    old = 0;
}

***** 时间11:41:04 GQOperation.m 第33行 *****
keyPath = executing, change = {
    kind = 1;
    new = 0;
    old = 1;
}

***** 时间11:41:04 GQOperation.m 第33行 *****
keyPath = finished, change = {
    kind = 1;
    new = 1;
    old = 0;
}

***** 时间11:41:04 ViewController.m 第36行 *****
operation 执行结束 <NSThread: 0x280ae7700>{number = 5, name = (null)}

***** 时间11:41:04 ViewController.m 第39行 *****
end <NSThread: 0x280ab2ec0>{number = 1, name = main}

***** 时间11:41:04 GQOperation.m 第37行 *****
Operation 释放了

这样就可以观察到任务的执行状态及所在线程情况, 还是运行在主线程之上的, Operation的取消并不能取消已经在运行的Operation。

依赖

在对剩余的属性及方法进行一波分析:

1
2
3
4
5
6
7
8
9
10
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;
- (void)waitUntilFinished API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

@property (readonly, getter=isAsynchronous) BOOL asynchronous API_AVAILABLE(macos(10.8), ios(7.0), watchos(2.0), tvos(9.0));
@property (readonly, getter=isReady) BOOL ready;
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
@property NSOperationQueuePriority queuePriority;
@property NSQualityOfService qualityOfService API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

先看这两个方法addDependency, removeDependency字面意思是添加依赖和移除依赖, 参数是另一个NSoperation我们可以尝试写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GQOperation *firstOperation = [[GQOperation alloc] init];
firstOperation.taskBlock = ^{
    NSLog(@"我是第一个operation");
};

GQOperation *secondOperation = [[GQOperation alloc] init];
secondOperation.taskBlock = ^{
    NSLog(@"我是第二个operation");
    [NSThread sleepForTimeInterval:2];
};

GQOperation *thirdOperation = [[GQOperation alloc] init];
thirdOperation.taskBlock = ^{
    NSLog(@"我是第三个operation");
    [NSThread sleepForTimeInterval:3];
};
[firstOperation addDependency:secondOperation];
[secondOperation addDependency:thirdOperation];
[firstOperation start];

然后程序崩溃了:

1
2
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[GQOperation start]: receiver is not yet ready to execute'
*** 

这里大概是说有一个Operation没有准备好运行, 刚巧上面有一个ready的属性可以判断试试:

1
2
3
if (secondOperation.isReady) {
    [firstOperation start];
}

果然, 程序不会在崩溃了, 如果我们将Operation的倒序执行的话:

1
2
3
4
5
6
7
8
9
10
[firstOperation addDependency:secondOperation];
[secondOperation addDependency:thirdOperation];
NSLog(@"second Ready? = %d", secondOperation.isReady);
[thirdOperation start];
NSLog(@"second Ready? = %d", secondOperation.isReady);
[secondOperation start];
NSLog(@"second Ready? = %d", secondOperation.isReady);
if (secondOperation.isReady) {
    [firstOperation start];
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
***** 时间14:45:28 ViewController.m 66 *****
second Ready? = 0

***** 时间14:45:28 ViewController.m 61 *****
我是第三个operation

***** 时间14:45:31 ViewController.m 68 *****
second Ready? = 1

***** 时间14:45:31 ViewController.m 55 *****
我是第二个operation

***** 时间14:45:33 ViewController.m 70 *****
second Ready? = 1

***** 时间14:45:33 ViewController.m 50 *****
我是第一个operation

可以看到, 这些Operation之间是有相互依赖关系的, 只有依赖的Operation执行完毕之后才能进行另一个Operation, 往往我的业务中也有很多这样的特点, 比如网络请求的依赖等。

还剩下一些优先级参数和同步异步参数会放到NSOperationQueue中讲到。

NSOperationQueue

由于之前的操作都是在主线程上进行(NSBlockOperation有例外, 他的add方法不一定是在主线程, 有可能是其他线程, 由于我只专注了自定义Operation疏忽了对系统的两个对象的使用), 感觉和多线程没有什么关系, 如果要实现多线程编程, 还需要借助另外一个对象NSOperationQueue

基础操作

首先我们来创建一个NSOperationQueue, 添加两个Operation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSLog(@"主线程开始");
GQOperation *firstOperation = [[GQOperation alloc] init];
firstOperation.taskBlock = ^{
    NSLog(@"我是自定义Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    NSLog(@"我是自定义Operation 延迟输出  %@", [NSThread currentThread]);
};

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:firstOperation];
[queue addOperationWithBlock:^{
    NSLog(@"我是 Block Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:2];
    NSLog(@"我是 Block Operation 延迟输出  %@", [NSThread currentThread]);
}];
NSLog(@"主线程结束");

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
***** 时间16:03:51 ViewController.m 第75行 *****
主线程开始

***** 时间16:03:51 ViewController.m 第90行 *****
主线程结束

***** 时间16:03:51 ViewController.m 第86行 *****
我是 Block Operation  <NSThread: 0x282617480>{number = 7, name = (null)}

***** 时间16:03:51 ViewController.m 第78行 *****
我是自定义Operation  <NSThread: 0x282623600>{number = 6, name = (null)}

***** 时间16:03:52 ViewController.m 第80行 *****
我是自定义Operation 延迟输出  <NSThread: 0x282623600>{number = 6, name = (null)}

***** 时间16:03:53 ViewController.m 第88行 *****
我是 Block Operation 延迟输出  <NSThread: 0x282617480>{number = 7, name = (null)}

可以看到, NSOperationQueue默认是一个异步并发的队列, 并且加入到队列中的Operation不用执行start方法, 队列会帮我们自动执行。类似于GCD的调度组。

suspended

可以使用suspended使队列中的任务暂停:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSLog(@"主线程开始");
GQOperation *firstOperation = [[GQOperation alloc] init];
firstOperation.taskBlock = ^{
    NSLog(@"我是自定义Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    NSLog(@"我是自定义Operation 延迟输出  %@", [NSThread currentThread]);
};

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:firstOperation];

queue.suspended = true;

[queue addOperationWithBlock:^{
    NSLog(@"我是 Block Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:2];
    NSLog(@"我是 Block Operation 延迟输出  %@", [NSThread currentThread]);
}];
NSLog(@"主线程结束");

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    queue.suspended = false;
});

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
***** 时间16:46:28 ViewController.m 75 *****
主线程开始

***** 时间16:46:28 ViewController.m 93 *****
主线程结束

***** 时间16:46:28 ViewController.m 78 *****
我是自定义Operation  <NSThread: 0x28247ff40>{number = 6, name = (null)}

***** 时间16:46:29 ViewController.m 80 *****
我是自定义Operation 延迟输出  <NSThread: 0x28247ff40>{number = 6, name = (null)}

***** 时间16:46:33 ViewController.m 89 *****
我是 Block Operation  <NSThread: 0x28247ff40>{number = 6, name = (null)}

***** 时间16:46:35 ViewController.m 91 *****
我是 Block Operation 延迟输出  <NSThread: 0x28247ff40>{number = 6, name = (null)}

这样也说明了任务被添加之后就开始了。

maxConcurrentOperationCount

maxConcurrentOperationCount这个属性可以控制最大的并发任务数量, 如果将这个值设置为1则可实现串行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 NSLog(@"主线程开始");
GQOperation *firstOperation = [[GQOperation alloc] init];
firstOperation.taskBlock = ^{
    NSLog(@"我是自定义Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    NSLog(@"我是自定义Operation 延迟输出  %@", [NSThread currentThread]);
};
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
[queue addOperation:firstOperation];
[queue addOperationWithBlock:^{
    NSLog(@"我是 Block Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:2];
    NSLog(@"我是 Block Operation 延迟输出  %@", [NSThread currentThread]);
}];
NSLog(@"主线程结束");

可以看到结果是异步的顺序输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
***** 时间17:10:11 ViewController.m 75 *****
主线程开始

***** 时间17:10:11 ViewController.m 91 *****
主线程结束

***** 时间17:10:11 ViewController.m 78 *****
我是自定义Operation  <NSThread: 0x280675b00>{number = 5, name = (null)}

***** 时间17:10:12 ViewController.m 80 *****
我是自定义Operation 延迟输出  <NSThread: 0x280675b00>{number = 5, name = (null)}

***** 时间17:10:12 ViewController.m 87 *****
我是 Block Operation  <NSThread: 0x280675b00>{number = 5, name = (null)}

***** 时间17:10:14 ViewController.m 89 *****
我是 Block Operation 延迟输出  <NSThread: 0x280675b00>{number = 5, name = (null)}

waitUntilFinished

之前说还有一部分NSOperation的属性和方法没有分析, 其实他们在队列对象中也有类似的方法, 比如waitUntilFinished, 他主要是起到一个阻塞当前线程的效果, 等待到完成之后才会执行其他任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NSLog(@"主线程开始");
GQOperation *firstOperation = [[GQOperation alloc] init];
firstOperation.taskBlock = ^{
    NSLog(@"我是自定义Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    NSLog(@"我是自定义Operation 延迟输出  %@", [NSThread currentThread]);
};

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 注释部分和下面这一句是同样的效果
[queue addOperations:@[firstOperation] waitUntilFinished:true];
// [queue addOperation:firstOperation];
// [firstOperation waitUntilFinished];

[queue addOperationWithBlock:^{
    NSLog(@"我是 Block Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:2];
    NSLog(@"我是 Block Operation 延迟输出  %@", [NSThread currentThread]);
}];
NSLog(@"主线程结束");

这个得到结果是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
***** 时间17:31:39 ViewController.m 第75行 *****
主线程开始

***** 时间17:31:39 ViewController.m 第78行 *****
我是自定义Operation  <NSThread: 0x28167d940>{number = 6, name = (null)}

***** 时间17:31:40 ViewController.m 第80行 *****
我是自定义Operation 延迟输出  <NSThread: 0x28167d940>{number = 6, name = (null)}

***** 时间17:31:40 ViewController.m 第92行 *****
主线程结束

***** 时间17:31:40 ViewController.m 第88行 *****
我是 Block Operation  <NSThread: 0x28167d940>{number = 6, name = (null)}

***** 时间17:31:42 ViewController.m 第90行 *****
我是 Block Operation 延迟输出  <NSThread: 0x28167d940>{number = 6, name = (null)}

当然, 如果我们不需要异步并发的操作, 可以直接使用NSOperation对象, 然后调用这些方法。

queuePriority

queuePriority, 任务优先级, 默认创建的任务都是Normal级别, 可以验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NSLog(@"主线程开始");
GQOperation *firstOperation = [[GQOperation alloc] init];
// 观察任务的第一条输出 如果指定了高的优先级 总是会先出现自定义Operation的第一条输出 即使自定义的Operation是后添加进去的 如果指定了低的优先级 总是会先出现Block的第一条输出
// firstOperation.queuePriority = NSOperationQueuePriorityHigh;
firstOperation.queuePriority = NSOperationQueuePriorityLow;
firstOperation.taskBlock = ^{
    NSLog(@"我是自定义Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    NSLog(@"我是自定义Operation 延迟输出  %@", [NSThread currentThread]);
};

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

[queue addOperationWithBlock:^{
    NSLog(@"我是 Block Operation  %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:2];
    NSLog(@"我是 Block Operation 延迟输出  %@", [NSThread currentThread]);
}];
[queue addOperation:firstOperation];
NSLog(@"主线程结束");

其余的待补充…