关键概念:

  • 回调模式
  • 回调地狱
  • Promise/Future 模式
  • PromiseKit - Max Howell
  • Bolts/Tasks - Facebook

常见回调模式

一般是一个地方调用,然后需要在另一个地方等待回调,Objective-C 里有时会在 block 里等待回调。举个栗子🌰,我们来到兰州拉面馆,

  1. “老板给我拉个面”“大份小份”“大份大份”“好嘞,马上就好”(成功发布一个制作拉面的任务),
  2. 然后我们找个地方坐下,看看手(妹)机(纸),
  3. 过了会儿,
  4. “客官,您要的面”只见一个兰州妹纸端过来一大碗正是刚才我们点的兰州拉面(此时既是异步任务完成,来到了回调节点)。
1
2
3
4
5
6
// 发布任务(触发节点)
[LanZhouMianGuan makeLaMianWithCompletion:^(BOOL finished) {
if (finished) {
// 制作完成(回调节点)
}
}];

回调地狱

回调模式是常见处理异步任务的一种模式,但是面对具有复杂依赖关系的异步任务时,回调模式因为任务触发点和回调点不在一个地方,或者 block 嵌套层数过多,导致内部逻辑十分复杂难以维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 发布任务(触发节点)
[LanZhouMianGuan makeLaMianWithCompletion:^(BOOL finished) {
if (finished) {
// 拉面制作完成(回调节点)
[LanZhouMianGuan makeDaPanJiWithCompletion:^(BOOL finished) {
if (finished) {
// 大盘鸡制作完成(回调节点)
[LanZhouMianGuan makeXiHongShiDanWithCompletion:^(BOOL finished) {
if (finished) {
// 西红柿蛋制作完成(回调节点)
[LanZhouMianGuan makeTuDouSiWithCompletion:^(BOOL finished) {
if (finished) {
// 土豆丝制作完成(回调节点)
}
}];
}
}];
}
}];
}
}];

Promise/Future 模式

Promise/Future 模式也可以用来处理异步任务协同,它有一个关键概念票据,票据里包含该异步任务完成程度的各种状态信息,发布命令时持有该票据,然后后续相关依赖的异步任务也都返回一个票据,然后只需要在一个地方根据持有的各种票据处理所有相关任务的依赖关系,既达到了在一个更高的抽象层单独维护这些依赖关系的效果,从而使得逻辑更清晰。

PromiseKit

这是一个开源库,作者 Max Howell,此人也是 Homebrew 的作者,这里有段故事,据说当年 Max 参加 Google 的面试,因不(拒)会(绝)写反转二叉树而没有拿到 offer,江湖中也算得上一号人物。PromiseKit 这个库还是蛮大的,据说为了完成这个库前后总共花费了 Max 数千小时的工作量,嗯,数千小时,假设1天2小时,一年365天的话,1000小时也得差不多一年半的时间,嗯,牛。PromiseKit 传送门

Bolts/Tasks

(咳咳,嗯,主角要出场了)今天我们要讲的这个 Bolts 乃是挂着互联网科技公司巨头 Facebook 的名号,嗯……听上去来头不小,那我们下面就看看它到底有神马能耐。Bolts-ObjC 俺们也有传送门

Bolts/Tasks 来者何人

  • 基于 Promise/Future 模式,用来处理复杂异步依赖关系
  • 能避免常见的代码回调地狱
  • Facebook 大厂出品
  • 基于 Objective-C,也很好地兼容到了 Swift
  • 并不是 NSOperation 或 GCD 的替代,而是作为补充可以一起配合使用
  • 简洁明了、易理解

嗯,嗯……其实我喜欢这个库是因为在 Objective-C 中,其使用场景的代码非常简洁、逻辑清晰,就一个字优雅,不对

Bolts/Tasks 内部原理

来来来,围观下这么神奇的功能到底是肿么实现的呢?

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
+ (instancetype)taskForCompletionOfAnyTask:(nullable NSArray<BFTask *> *)tasks
{
__block int32_t total = (int32_t)tasks.count;
if (total == 0) {
return [self taskWithResult:nil];
}
__block int completed = 0;
__block int32_t cancelled = 0;
NSObject *lock = [NSObject new];
NSMutableArray<NSError *> *errors = [NSMutableArray new];
BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource];
for (BFTask *task in tasks) {
[task continueWithBlock:^id(BFTask *t) {
if (t.error != nil) {
@synchronized(lock) {
[errors addObject:t.error];
}
} else if (t.cancelled) {
OSAtomicIncrement32Barrier(&cancelled);
} else {
if(OSAtomicCompareAndSwap32Barrier(0, 1, &completed)) {
[source setResult:t.result];
}
}
if (OSAtomicDecrement32Barrier(&total) == 0 &&
OSAtomicCompareAndSwap32Barrier(0, 1, &completed)) {
if (cancelled > 0) {
[source cancel];
} else if (errors.count > 0) {
if (errors.count == 1) {
source.error = errors.firstObject;
} else {
NSError *error = [NSError errorWithDomain:BFTaskErrorDomain
code:kBFMultipleErrorsError
userInfo:@{ @"errors": errors }];
source.error = error;
}
}
}
// Abort execution of per tasks continuations
return nil;
}];
}
return source.task;
}

OSAtomic 原子操作,嗯……if else嗯……牛🐂!

Bolts/Tasks 怎么玩

举个栗子🌰,蓝牙中心模式中假如需要三个步骤来完成一次对特定外设的写入命令,首先要获取命令密钥,然后去扫描匹配并连接外设,最后才能写入命令,每一步我们都假定它是异步的,这三个异步操作的依赖关系相当于是串行发生的。

首先是三个异步任务,里面用延时技术模拟这种方式完成操作所需的时间。

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
+ (BFTask *) fetchCommandKeysAsync {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
NSLog(@"Fetch command keys starting...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Fetch command keys complete!");
[tcs setResult:nil];
});
return tcs.task;
}
+ (BFTask *) findBikeAsync {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
NSLog(@"Find bike starting...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Find bike complete!");
[tcs setResult:nil];
});
return tcs.task;
}
+ (BFTask *) writeCommandToBikeAsync {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
NSLog(@"Write command to bike starting...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Write command to bike complete!");
[tcs setResult:nil];
});
return tcs.task;
}

对已经完成的 tcs 设置 result 或 error 会报异常,建议使用 try 的版本,如 trySetResult 和 trySetError,然后我们把这些异步服务组合成一个操作流程,

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (void)bluetoothOpenLockFlow {
BFTask *task = [self fetchCommandKeysAsync];
[[[task continueWithBlock:^id _Nullable(BFTask *task) {
NSLog(@"Flow - Fetch command keys complete!");
return [self findBikeAsync];
}] continueWithBlock:^id _Nullable(BFTask *task) {
NSLog(@"Flow - Find bike complete!");
return [self writeCommandToBikeAsync];
}] continueWithBlock:^id _Nullable(BFTask *task) {
NSLog(@"Flow - Write command to bike complete!");
return nil;
}];
}

让我们看看调试日志,

1
2
3
4
5
6
7
8
9
19:28:16.57 Fetch command keys starting...
19:28:22.00 Fetch command keys complete!
19:28:22.00 Flow - Fetch command keys complete!
19:28:22.00 Find bike starting...
19:28:27.26 Find bike complete!
19:28:27.26 Flow - Find bike complete!
19:28:27.26 Write command to bike starting...
19:28:32.26 Write command to bike complete!
19:28:32.26 Flow - Write command to bike complete!

是不是很清晰,哈哈,现在假如我们有网络开锁、蓝牙开锁、面容解锁三种开锁方式,网络状况好的时候,第一种方式开锁最快,弱网断网时蓝牙开锁也不慢,蓝牙设备距离远信号弱时面容解锁也可以,具体的,我们只需发送一次开锁命令,不管哪种方式开锁成功了,我们就算开锁成功,为了完成这个任务,我们首先需要创建三只异步开锁函数,

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
- (BFTask *) networkOpenLockAsync {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
NSLog(@"Network open lock starting...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Network open lock complete!");
[tcs setResult:nil];
});
return tcs.task;
}
- (BFTask *) bluetoothOpenLockAsync {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
NSLog(@"Bluetooth open lock starting...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Bluetooth open lock complete!");
[tcs setResult:nil];
});
return tcs.task;
}
- (BFTask *) faceOpenLockAsync {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
NSLog(@"Face open lock starting...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Face open lock complete!");
[tcs setResult:nil];
});
return tcs.task;
}

接着,我们需要用到 taskForCompletionOfAnyTask: 这个函数来完成这个逻辑,既任何一个 task 完成,就告诉发送开锁命令的人,我们已经开锁成功了。

1
2
3
4
5
6
7
8
9
10
11
- (void)openLockFlow {
NSMutableArray *tasks = [NSMutableArray array];
[tasks addObject: [self networkOpenLockAsync]];
[tasks addObject: [self bluetoothOpenLockAsync]];
[tasks addObject: [self faceOpenLockAsync]];
BFTask *task = [BFTask taskForCompletionOfAnyTask:tasks];
[task continueWithBlock:^id _Nullable(BFTask *task) {
NSLog(@"Open lock complete!");
return nil;
}];
}

Demo 的话可以这样放置命令入口:

1
2
3
4
5
- (void)viewDidLoad {
[super viewDidLoad];
[self openLockFlow];
}

我们可以看到这次面容解锁很给力,只用了5秒钟就开锁成功了,嗯……明明可以靠脸吃饭 却偏偏要靠才华!

1
2
3
4
5
6
7
12:48:59.60 Network open lock starting...
12:48:59.60 Bluetooth open lock starting...
12:48:59.60 Face open lock starting...
12:49:04.60 Face open lock complete!
12:49:04.60 Open lock complete!
12:49:10.59 Bluetooth open lock complete!
12:49:16.04 Network open lock complete!

好的,今天就分享到这里吧!谢谢 各位看官辛苦了💦!如果喜欢请赏个雪糕🍦吃呗大爷,打个赏呗!