解释下,题目好像取的有点大了,异步编程… 文章的内容涉及一个问题和 PromiseKit 的源码分析。

由一个问题引发的思考。我们在操作一系列的网络请求并且这些网络请求之前存在依赖关系。比如:

  1. 用户登陆完之后服务端返回 token
  2. 获取用户的联系人的列表(需要 token 认证)

以上两个网络请求中间就涉及到 token 衔接,我们很容易想到这个问题,通过一次嵌套就能完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
APICilent.login(url: "https://xxxx/api/login", params: paramDict, compelationHandler: {result, reponse, error in 
if let token = result["token"] {
APICilent.fetchContact(url: "https://xxxx/api/contact", params: paramDict, compelationHandler:{result, reponse, error in
if let result = result {

} else {
// error
}
})
} else {
// error
}
})

此时我们只有两个依赖请求,如果这样的请求很多个的话,我们就陷入了回调地狱(也就是出现大量的 } 这样的符号)在 else 中无法看清是哪个对应关系。这样的代码既不美观也很难维护。这个问题在最早发生在 JavaScript,所以我们有时看到原生的 JavaScript 代码中就含有大量的这样的 ‘又臭又长’ 的代码。
但是在 Cocoa 框架中好像没有很好的解决方法。直到有一天,我看到 ES6 的新特性 Promise。

Promise 是异步编程的一种解决方案。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果

Promise 对象有两个特点:

  • 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态,只有异步操作可以决定哪种状态,其他操作无法改变这个状态。
    • Pending 进行中
    • Resolve 已完成,又称 Fulfill
    • Rejected 已失败
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变只有两种可能:
    • Pending 变为 Resolved
    • Pending变为 Rejected

then 方法

Promise 对象的一个方法 then 它为 Promise 添加一个回调函数,then 方法返回一个新的 Promise 实例,因此可以采用链式写法。

1
2
3
4
5
6
7
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function funcA(comments) {
console.log("Resolved: ", comments);
}, function funcB(err){
console.log("Rejected: ", err);
});

catch 方法

catch 方法是 then 方法的一个别名,用于发生错误时的回调函数,如果异步操作抛出异常,状态就会变成 Rejected,就会调用 catch 方法。

1
2
3
4
5
6
getJSON("/posts.json").then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

以上是简单的介绍了一下 Promise,在 OC 和 Swift 中也有类似的第三方库 —— PromiseKit。基本的思想与 Promise 相似,也有 PendingResolvedRejected 三个状态,同样有 thencatch 等等,但是在 PromiseKit 框架中还有 always 方法(在一次请求中总是执行的方法)。
接下来我们看看使用 PromiseKit 是如何优化多个依赖请求的(这里的优化指的是在代码结构上上的优化,并非性能上的优化)。

1
2
3
4
5
6
7
8
9
10
11
12
13
let url = URLRequest(url: URL(string: "https://httpbin.org/ip")!)

Alamofire.request(url).responseJSON().then { json in
print("token = \(json)")

let param = self.parseJson(json)
return AnyPromise(self.login(param))

}.then { data in
print("login = \(data!)")
}.catch { error in
print("error")
};

从上面的代码来看,回调地狱就解决了,没有那么多的 } ,并且代码的结构和可读性也增强了。PromiseKit 在各个异步操作间传递的是 Promise 对象,对应着各个层级的异步操作方法 then 。我们来简单分析下源码。

源码分析

1
2
3
4
5
6
7
8
// 代码-1
let url = URLRequest(url: URL(string: "https://httpbin.org/ip")!)

Alamofire.request(url).responseJSON().then { json in
print("token = \(json)")
}.catch { error in
print("error = \(error)")
}

PromiseKit 框架的中的主要类:

  • Promise
  • State

State 还有两个子类 SealedState 和 UnealedState。
我们在使用该框架前,需要去封装一些方法,比如 Alamofire 网络库,我们就需要做以下的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 代码-2
/// Adds a handler to be called once the request has finished.
public func responseJSON(_ options: JSONSerialization.ReadingOptions = .allowFragments) -> Promise<Any> {
return Promise { fulfill, reject in
responseJSON(queue: nil, options: options, completionHandler: { response in
switch response.result {
case .success(let value):
fulfill(value)
case .failure(let error):
reject(error)
}
})
}
}

responseJSON() 是 Alamofire 请求的结果回调函数,这里我们扩展这些回调函数,并返回一个 Promise<Any> 。方法的实现中实例化一个 Promise 对象,fulfillreject 请求成功回调、失败回调。然后根据请求的结果来调用相应的闭包。这里 case .success(let value) 作为模式匹配来取得请求成功的数据;相应的, case .failure(let error) 获取错误的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 代码-3
required public init(resolvers: (_ fulfill: @escaping (T) -> Void, _ reject: @escaping (Error) -> Void) throws -> Void) {
var resolve: ((Resolution<T>) -> Void)!
do {
state = UnsealedState(resolver: &resolve)
try resolvers({
resolve(.fulfilled($0))
}, { error in
#if !PMKDisableWarnings
if self.isPending {
resolve(Resolution(error))
} else {
NSLog("PromiseKit: warning: reject called on already rejected Promise: \(error)")
}
#else
resolve(Resolution(error))
#endif
})
} catch {
resolve(Resolution(error))
}
}

从函数签名来看是一个构造函数,创建一个 pending 状态的 Promise 对象, fulfillreject 一个元组作为参数,其中初始化了一个成员变量 stateUnsealedState(resolver:) 传递一个 inout 类型也就是引用类型变量,resolve 闭包是将任务结果传递给 state 变量,来处理任务状态的变化。这一步非常重要,也就是我们之前所说的 Promise 的状态转移(pending 转变到 fulfill 或者 reject,后边我们再讲述 state 中的代码)。这里的 .fulfilled($0) 也就是我们 代码-2 中 value 值。将 value 值也就是成功的数据做一层包装,说明该值处于成功状态,并且成功状态的数据是 $0 。第二个回调是失败的状态,如果现在 Promise 的状态是是 Pending 说明任务还没解决,如果进入这个判断里,说明任务失败,比如网络请求失败。

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
// 代码-4
class UnsealedState<T>: State<T> {
fileprivate let barrier = DispatchQueue(label: "org.promisekit.barrier", attributes: .concurrent)
fileprivate var seal: Seal<T>

...

required init(resolver: inout ((Resolution<T>) -> Void)!) {
seal = .pending(Handlers<T>())
super.init()
resolver = { resolution in
var handlers: Handlers<T>?
self.barrier.sync(flags: .barrier) {
if case .pending(let hh) = self.seal {
self.seal = .resolved(resolution)
handlers = hh
}
}
if let handlers = handlers {
for handler in handlers {
handler(resolution)
}
}
}
}
}

这个类就是 代码-3 中那个关键的一步 UnsealedState(resolver:) 参数是一个 inout 类型。该类的成员变量 seal 和 barrier,分别是表示 Promise 未处理的状态和表示一个 GCD 调度队列。此时这里的 resolution 变量也就是 代码-3 中的 .fulfilled($0) ,handlers 保存着还未处理的任务,再以同步的方式处理状态的变化,在 seal 中获取 .pending 状态的任务。然后将结果 resolution 传递给 handler。

1
2
3
4
5
6
7
8
// 代码-5
open func then<U>(on q: DispatchQueue = .default, execute body: @escaping (T) throws -> U) -> Promise<U> {
return Promise<U> { resolve in
state.then(on: q, else: resolve) { value in
resolve(.fulfilled(try body(value)))
}
}
}

这个 then 方法是 代码-1 中的 then,同样的该方法也是返回一个 Promise 对象,来处理已解决的任务。

Parameter body: The closure that is executed when this Promise is fulfilled

更加具体一些是处理成功回调,通过 state 对象的 then 方法,来进一步处理任务。

1
2
3
4
5
6
// 代码-6
init(sealant: (@escaping (Resolution<T>) -> Void) -> Void) {
var resolve: ((Resolution<T>) -> Void)!
state = UnsealedState(resolver: &resolve)
sealant(resolve)
}

类似的初始化一个 Promise 对象,类似 代码-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
// 代码-7
// State<T> 中的方法
final func then<U>(on q: DispatchQueue, else rejecter: @escaping (Resolution<U>) -> Void, execute body: @escaping (T) throws -> Void) {
pipe { resolution in
switch resolution {
case .fulfilled(let value):
contain_zalgo(q, rejecter: rejecter) {
try body(value)
}
case .rejected(let error, let token):
rejecter(.rejected(error, token))
}
}
}

func get(_ body: @escaping (Seal<T>) -> Void) { fatalError("Abstract Base Class") }

final func pipe(_ body: @escaping (Resolution<T>) -> Void) {
get { seal in
switch seal {
case .pending(let handlers):
handlers.append(body)
case .resolved(let resolution):
body(resolution)
}
}
}

// UnsealedState<T> 中的方法
override func get(_ body: @escaping (Seal<T>) -> Void) {
var sealed = false
barrier.sync {
switch self.seal {
case .resolved:
sealed = true
case .pending:
sealed = false
}
}
if !sealed {
barrier.sync(flags: .barrier) {
switch (self.seal) {
case .pending:
body(self.seal)
case .resolved:
sealed = true // welcome to race conditions
}
}
}
if sealed {
body(seal) // as much as possible we do things OUTSIDE the barrier_sync
}
}

代码-5 中 state 调用的 then 方法,pipe 方法参数闭包中的 contain_zalgo 方法是 PromiseKit 中的最深层的方法。pipe 方法中又调用子类 UnsealedState<T> 的 get 方法,同时它将闭包传递出去,再在 get 方法中判断任务是否解决,已经解决,则闭包回调 seal 至 pipe 方法,此时 seal 处于 pending 状态,则加入到 handlers 中。如果以已经解决,将 pipe 方法参数中的 body 回调至 then 方法,判断任务解决的结果成功还是失败,再做相应的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 代码-8
enum Seal<T> {
case pending(Handlers<T>)
case resolved(Resolution<T>)
}

enum Resolution<T> {
case fulfilled(T)
case rejected(Error, ErrorConsumptionToken)

init(_ error: Error) {
self = .rejected(error, ErrorConsumptionToken(error))
}
}

上面两个 enum 是对每个异步操作一层包装,说明每个操作当前所处的状态,也就是我们上面所提到的三种状态。通过对任务或者任务结果的包装,再通过类似 case .fulfilled(let value) 匹配来获取的正处于某种状态的数据。

1
2
3
4
// 代码-9
class Handlers<T>: Sequence {
var bodies: [(Resolution<T>) -> Void] = []
}

Handlers 来保存还未解决的任务。并且该任务是 (Resolution<T>) -> Void 这样的闭包。

小结

Alamofire.request(url).responseJSON() 这个部分通过封装网络库 Alamofire 的响应方法 responseJSON(),回调到 UnsealedState 类中,像 Alamofire.request(url).responseJSON().then,在后面链式调用一个 then 方法,则会在 handers 中增加一个未处理的任务闭包,等网络请求返回时。通过 case .pending(let hh) = self.seal 筛选出未处理的闭包,然后处理,这部分处理其实就是将结果分发到各自的闭包。这样一个 Promise 就结束了。
我们发现 PromiseKit 的 swift 实现使用了大量的闭包,导致源码阅读起来比较绕。所以定期的多读几遍收获还是很多的。但是对于闭包来讲,如果语义上合适,尾随闭包会增加代码的可读性;反之,反而让代码晦涩难懂。

总结

对于 PromiseKit 我们会发现这个库和 RxSwift 很类似。他们都是基于函数式编程思想。它们同样是对异步操作的管理。对于语义来说,也许 PromiseKit 更好理解一些。 比如

  • then 方法处理接下里做什么
  • always 方法来处理一个总是要操作的步骤,比如:网络请求的 loading 动画
  • when 当几个 promise 都获取到结果时,做什么?
  • catch 方法处理 error

对于 RxSwift 而言 mapflatMap 等等,概念和理解都要比 PromiseKit 要辛苦一点。但是 PromiseKit 的缺点也很明显,需要自己去实现相应的 category,比如上面 Alamfire 的网络请求,就需要我们再封装一层才能使用。

参考

github-PromiseKit
iOS如何优雅的处理“回调地狱Callback hell”
Swift 并行编程现状和展望

写在最后

希望对你有帮助,写的不正确的地方,欢迎拍砖~