PromiseKit 大家都在项目中见过,是典型的一看就会,一用就废的技术。写 PromiseKit 代码想必大家都经历过如下图的错误支配的恐惧。
通过本文,让我们深入浅出重新认识 PromiseKit。
概念考古
Promise 是异步编程的一种解决方案,比传统的解决方案------回调函数和事件------更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise
对象。
所谓 Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise
对象有以下两个特点:
- 对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是"承诺",表示其他手段无法改变。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了 Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
PromiseKit
Promise 是对异步操作的封装。状态固定。Promise 对异步返回的结果与错误进行了封装。
PromiseKit 是 Max Howell(brew 作者)小而美的作品。PromiseKit 为 Swift 带来了 Promise 的语义实现,对于 Swift 来说,更重要的意义是:
- 提升代码可读性。封装 与链式拼接异步逻辑,避免写出回调地狱代码。同时也便于在异步流程中插入或删除逻辑。
- 便捷地多线程调度方法。
简单来说 Promise 就是接收上一级的结果,处理,异步返回结果。这是在 Swift 5.5+ async/await 之前推荐的异步逻辑封装方式。
与 Rx 的区分
最显著:
- Promise 返回一个结果。
- Observable 返回多个结果,是一个序列流。
初衷:
- Rx 致力于改变编程方式,把代码重构为可交互的管道式矩阵。
- Promise 只着眼于异步任务的管理。
实现上:
- Promise 的所有元素都使用相同的模式。Rx 则非常全面地提供了内部元素的相互操作。
- Rx 构建的事件链条不一定会自动终止,所以需要承担一定的垃圾回收。而 Promise 都会生成一个状态,终止时则释放自身。
应用场景
适用:
- 可能异步,返回一个结果,或失败,或成功。
不适用:
- 返回多个结果的序列流。
即要使用 PromiseKit,需要把要封装的逻辑抽象成一个异步事件,这个事件只有一个结果。
核心类型
首先理解一些关键词:
- Promise:一个异步行为的封装对象。
- Guarantee:与 Promise 类似,也是异步行为的封装对象,但不会产生错误。
- pending:待定,Promise 的初始状态。
- resolved:Promise 的结束状态。结束状态又可分为:
- fulfill:完成,Promise 的成功状态。
- reject:拒绝,Promise 的失败状态。
这些关键词都会在 PromiseKit 的 API 中高频出现。
Result
PromiseKit 中表达结果的枚举,与 Swift 标准库中的 Result 有异曲同工、不同风格的表达方式。如其中的定义,结果只有成功和失败两个状态。
Swift
enum Result<T> {
case fulfilled(T)
case rejected(Error)
}
Resolver
可以理解为是生成 Result 的对象,用于构造 Promise 对象。提供了 fulfill
和 reject
方法标记成功和失败的结果。
Swift
func fulfill(_ value: T)
func reject(_ error: Error)
func resolve(_ result: Result<T>)
func resolve(_ obj: T?, _ error: Error?)
func resolve(_ obj: T, _ error: Error?)
func resolve(_ error: Error?, _ obj: T?)
Promise
可能会失败的异步封装对象。范型类型,范型是指成功值的类型。
Swift
class Promise<T>: Thenable, CatchMixin
Guarantee
不会抛出错误的异步封装对象。范型类型,范型是结果值的类型。
Swift
class Guarantee<T>: Thenable
两者的区别可以用图表示为:
Thenable
为 Promise 和 Guarantee 对象都遵循的协议,提供了拼接 Promise/Guarantee 并提供其他原语的能力。
Swift
/// 拼接 Promise/Guarantee,当其状态为成功时,执行拼接的 Promise/Guarantee。body 中返回的类型不必跟上一级任务的一致。
func then<U: Thenable>(_ body: @escaping(T) throws -> U) -> Promise<U.T>
/// 获取值,表示成功结束。注意其 body 不用返回值,后续也不能拼接获取值的 Promise/Guarantee。
func done(_ body: @escaping(T) throws -> Void) -> Promise<Void>
/// 获取值。也是只是获取值,且 body 中不用返回,后续可以继续拼接获取值的 Promise/Guarantee,即不会对后续拼接流程有副作用。
func get(_ body: @escaping (T) throws -> Void) -> Promise<T>
/// 获取 Result 对象,且 body 中不用返回。同样也是不会对后续拼接流程产生副作用。
func tap(_ body: @escaping(Result<T>) -> Void) -> Promise<T>
/// 值转换,同样要求状态为成功时才执行。
func map<U>(_ transform: @escaping(T) throws -> U) -> Promise<U>
func map<U>(_ keyPath: KeyPath<T, U>) -> Promise<U>
func compactMap<U>(_ transform: @escaping(T) throws -> U?) -> Promise<U>
func compactMap<U>(_ keyPath: KeyPath<T, U?>) -> Promise<U>
以上的接口都包含这些参数,为了便于阅读进行了省略:
on: DispatchQueue? = conf.Q.map
flags: DispatchWorkItemFlags? = nil
Swift
class Guarantee<T>: Thenable
CatchMixin
Promise 遵循了 CatchMixin,也是跟 Guarantee 的重要区别,即 Promise 可以失败和处理失败。遵循了 CatchMixin 的 Promise 可以通过 catch
原语统一处理拼接的 Promise。一旦有错误就会落入 catch
原语中。
Swift
/// 统一处理错误。与 done 类似,body 中也是不会返回。catch 后面只能再拼接 `PMKFinalizer.finally(on:flags:_:)`,不能再拼接其他 Promise。
func `catch`(_ body: @escaping(Error) -> Void) -> PMKFinalizer
/// 错误处理和恢复,body 中返回 Promise,使其可以后续拼接其他 Promise 执行。
func recover<U: Thenable>(_ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T
func recover(_ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T>
/// 获取值或错误,无论成功与否都会进入。body 中不用返回,不会对后续拼接流程产生副作用。用于在 catch 之前拼接;final 则在 catch 之后拼接。
func ensure(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) -> Promise<T>
以上的接口都包含这些参数,为了便于阅读进行了省略:
on: DispatchQueue? = conf.Q.map
flags: DispatchWorkItemFlags? = nil
policy: CatchPolicy = conf.catchPolicy
每个 promise 都是一个表示单个(individual)异步任务的对象。如果任务失败,它的 promise 将成为 rejected
。产生 rejected
promises 将跳过后面所有的 then
,而是将执行 catch
(严格上说是执行后续所有的 catch
处理)。
全局函数
Swift
/// 语法糖,用来包装一个 Promise/Guarantee,只是简单返回。
func firstly<U: Thenable>(execute body: () throws -> U) -> Promise<U.T>
func firstly<T>(execute body: () -> Guarantee<T>) -> Guarantee<T>
/// 把多个 promise 并联,并行执行,都完成后执行后面任务。类似于使用 DispatchGroup,内部使用 barrier 封装。
func when<U: Thenable>(fulfilled thenables: [U]) -> Promise<[U.T]>
/// 与 when 相反,当有最先完成的就会执行后面的任务。
func race<U: Thenable>(_ thenables: [U]) -> Promise<U.T>
使用
PromiseKit 使用的难点在于构造和拼接。使用图来表示:
写成代码:
这里值得注意的是,由于使用原语连接时是承接上一个 Promise 的成功结果的,并且给下一个连接的 Promise 提供入参,这承上启下的类型一定要对应上,不然就会类型不匹配的编译错误。
构造
Promise
static Promise.value(_:)
:用值构造已成功的 Promise 对象。
- 用于直接返回包含成功值的 Promise 对象。
Swift
static func value(_ value: T) -> Promise<T>
guard foo else {
return .value(bar)
}
Promise.init(error:)
:用错误对象构造已失败的 Promise 对象。
- 用于直接返回包含错误的 Promise 对象。
Swift
init(error: Error)
Promise.init(resolver:)
:使用闭包构造 Promise 对象。
- 用于把异步方法封装成 Promise 对象。
- 对于要求返回 Promise 对象的场景,可以充分利用 Swift 的类型推断机制来减少类型声明。
Swift
init(resolver body: (Resolver<T>) throws -> Void)
let p3 = Promise<Void> { seal in
check { seal.fulfill_() }
}
let p4 = Promise<String> { seal in
fetch { result, error in
seal.resolve(result, error)
}
}
class Promise.pending()
:使用 pending 元组来构建 Promise 对象。
- 用于在多个异步回调中拼接 Promise 对象。
- 对比
Promise.init(resolver:)
,可以减少闭包的嵌套。
Swift
class func pending() -> (promise: Promise<T>, resolver: Resolver<T>)
func fileExistsAsync(forKey key: String) -> Promise<Bool> {
let path = path(forKey: key)
let pending = Promise<Bool>.pending()
queue.addOperation {
let result = FileManager.default.fileExists(atPath: path)
def.resolver.resolve(result, nil)
}
return pending.promise
}
多次重试示例:
Swift
func attempt<T>(maximumRetryCount: Int = 3, delayBeforeRetry: DispatchTimeInterval = .seconds(2), _ body: @escaping () -> Promise<T>) -> Promise<T> {
var attempts = 0
func attempt() -> Promise<T> {
attempts += 1
return body().recover { error -> Promise<T> in
guard attempts < maximumRetryCount else { throw error }
return after(delayBeforeRetry).then(on: nil, attempt)
}
}
return attempt()
}
attempt(maximumRetryCount: 3) {
fetch(url: url)
}.then {
//...
}.catch { _ in
// we still failed
}
Guarantee
方法基本与 Promise 类似,区别是不会产生错误,因此语法更加简单。
Swift
/// 使用值构造 Guarantee 对象。
static func value(_ value: T) -> Guarantee<T>
/// 使用闭包构造 Guarantee 对象。
init(resolver body: (@escaping(T) -> Void) -> Void)
/// 使用 pending 元组来构建 Guarantee 对象。
class func pending() -> (guarantee: Guarantee<T>, resolve: (T) -> Void)
示例
下面以请求相册权限和保存图片到相册两个异步事件为例,来演示 PromiseKit 的使用。
请求相册权限
先来看看不使用 PromiseKit 的版本:
Swift
func requestPhotosAuthorityIfNeed(success: @escaping () -> Void, failure: @escaping (PhotosAuthorityError) -> Void) {
// 已授权的直接返回
guard !check(status: PHPhotoLibrary.authorizationStatus()) else {
success()
return
}
// 其他的进行权限请求
PHPhotoLibrary.requestAuthorization { status in
if check(status: status) {
success()
} else {
failure(error(status: status))
}
}
}
方法还用到了一些错误的定义和工具方法:
Swift
enum CommonError: Error {
case unknown
}
enum PhotosAuthorityError: Error {
case restricted, denied, unknown
}
func error(status: PHAuthorizationStatus) -> PhotosAuthorityError {
switch status {
case .restricted: return .restricted
case .denied: return .denied
default: return .unknown
}
}
func check(status: PHAuthorizationStatus) -> Bool {
switch status {
case .authorized: return true
default: return false
}
}
使用 Promise.init(resolver:)
方式构造:
Swift
func requestPhotosAuthorityIfNeed() -> Promise<Void> {
guard !check(status: PHPhotoLibrary.authorizationStatus()) else {
return .value(())
}
return Promise { seal in
PHPhotoLibrary.requestAuthorization { status in
if check(status: status) {
seal.fulfill_()
} else {
seal.reject(error(status: status))
}
}
}
}
使用 class Promise.pending()
方式构造,这种方式可以减少闭包嵌套的层数:
Swift
func requestPhotosAuthorityIfNeed_() -> Promise<Void> {
guard !check(status: PHPhotoLibrary.authorizationStatus()) else {
return .value(())
}
let pending = Promise<Void>.pending()
PHPhotoLibrary.requestAuthorization { status in
if check(status: status) {
pending.resolver.fulfill_()
} else {
pending.resolver.reject(error(status: status))
}
}
return pending.promise
}
保存图片到相册
先来看看不使用 PromiseKit 的版本:
Swift
func saveImageToAlbum(url: URL, success: @escaping () -> Void, failure: @escaping (Error?) -> Void) {
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
} completionHandler: { finished, error in
// 回调这里不为主队列,需切回主队列回调。
DispatchQueue.main.async {
if finished {
success()
} else {
failure(error)
}
}
}
}
使用 Promise.init(resolver:)
方式构造,Promise 使用原语拼接时回默认切回主队列,所以这里不需要 DispatchQueue.main.async
:
Swift
func saveImageToAlbum(url: URL) -> Promise<Void> {
Promise { seal in
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
} completionHandler: { finished, error in
if finished {
seal.fulfill_()
} else {
seal.reject(error ?? CommonError.unknown)
}
}
}
}
使用 class Promise.pending()
方式构造:
Swift
func saveImageToAlbum_(url: URL) -> Promise<Void> {
let pending = Promise<Void>.pending()
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)
} completionHandler: { finished, error in
if finished {
pending.resolver.fulfill_()
} else {
pending.resolver.reject(error ?? CommonError.unknown)
}
}
return pending.promise
}
组合使用
有了构造 Promise 的方法,那么要实现一个先请求相册权限,再保存图片的逻辑就很简单,同时还统一了成功和失败的处理逻辑:
Swift
firstly {
requestPhotosAuthorityIfNeed()
}.then {
saveImageToAlbum(url: url)
}.done {
print("Save successfully.")
}.catch { error in
print("Save failed, error: \(error)")
}
如果现存代码已经有 requestPhotosAuthorityIfNeed(success:failure:)
和 saveImageToAlbum(url:success:failure:)
这两个核心逻辑,我们也可以直接 inline 拼接 Promise 使用。这样,既能用到 PromiseKit 的优势,也避免了过度的封装。
Swift
firstly {
Promise<Void> { seal in
requestPhotosAuthorityIfNeed {
seal.fulfill_()
} failure: {
seal.reject($0)
}
}
}.then {
Promise<Void> { seal in
saveImageToAlbum(url: url) {
seal.fulfill_()
} failure: {
seal.reject($0 ?? CommonError.unknown)
}
}
}.done {
print("Save successfully.")
}.catch { error in
print("Save failed, error: \(error)")
}
最佳实践
聊完 PromiseKit 的基本使用,下面给出一些经验之谈,或许能让新同学避开不少坑。
- 保持
firstly
开头,虽然该函数虽然没有实质作用,但能让代码保持整齐和优雅。 - 必须明晰原语闭包中输入参数列表和输出类型,返回 Promise 对象的时候要对应上,否则出现编译错误。
done
、get
、tap
原语中的闭包无需返回值;但then
、map
、recover
原语都需在 body 闭包中返回 Promise/Guarantee 对象。当闭包中只有一行时可以直接省略 return 写 Promise 对象。但有多行,则还需补上 body 闭包的入参、返回类型和 return 的声明。典型的例子如,原本 then 闭包中只有一个 Promise 的构造函数,后来在 Promise 构造函数之前加了一行代码,如 print,就编译不过了,这就需要补充返回类型和 return 的声明。then
拼接的 body 中可以返回与上级 Promise/Guarantee 对象不同的范型类型,可以用于组合多个结果,这与map
会有些微妙的差别,如:
Swift
login().then { username in
fetch(avatar: username).map { ($0, username) }
}.then { image, username in
//...
}
- 尽可能避免多级闭包的嵌套,而是使用一个 Promise/Guarantee 对象表达一级异步闭包,即一个 Promise 只做一件事。使用链式拼接多级异步逻辑。这样更有利于后续插入或删除其中的逻辑。
- 优先选择 Promise 进行封装异步逻辑。Guarantee 不能返回错误,为了后续扩展,允许失败的 Promise 能提供更高的灵活性。
- 使用
catch
统一处理错误,而不是在单个 Promise 中处理。 - 要同时处理错误和成功值,使用
ensure
。finally
虽然语义类似,但不能接收上一步的结果。 - 使用
when
和race
处理多个 Promise/Guarantee 对象需同时执行的情况。 - 编写业务逻辑方法不用急于使用 Promise/Guarantee 对象封装,后续需要拼接的时候才进行封装,如:
Swift
firstly {
Promise<Void> { seal in
checkStorageSpaceByExportFileSize { seal.fulfill(()) }
}
}.then {
Promise<Void> { seal in
AuthorityManager.requestPhotos(with: "permission_request_album_export".L, success: { seal.fulfill(()) })
}
}.done {
self.presentExport(draft: draft, extra: self.makeExtraInfo())
}.catch { error in
}
- 使用
on
参数切换任务执行的线程队列,而不用另外封装 Promise/Guarantee 对象。所有的 Promise/Guarantee 都会在后台执行,但传递链本身(then()、catch()、map() 等)默认会在主线程执行,可以添加 on 参数执行的队列。这可以让封装的操作在指定的队列中执行。 - 使用
get
/tap
/then
插入不影响流程的逻辑,而不是在原有的闭包中插入。
[weak self] in PromiseKit
这个话题已经在 [swift - Should I use weak self] in PromiseKit blocks? - Stack Overflow 描述得很清晰,这里再啰嗦翻译一遍。
讨论在 PromiseKit 中是否需要使用 [weak self],其实是讨论在逃逸闭包中是否需要使用 [weak self]。非逃逸的就完全没有必要使用 [weak self] 哈。
先给出结论:PromiseKit 中,只有在需要在 self 释放时就终止闭包中的逻辑时,才使用 [weak self]。但无论何时尽可能避免使用 [unowned self]。
在闭包中使用 [weak self] 主要是为了:
- 防止引用循环(retain cycle);
- 防止延长 self 指向对象的生命周期。
Promise 创建时确实会捕获外部对象,使用 self 也会持有 self 指向的对象。但如文章开头说到的 Promise 一定会执行,且一定会结束,在结束时就释放闭包中捕获的对象。所以在使用 PromiseKit 的 API 时完全不用担心闭包对外部对象会造成循环引用问题,Promise 结束时自然就释放了。当然要是自己提供的闭包,则还是要小心考虑。
Promise 会在结束的时候才会对闭包捕获的对象释放,这意味着若 Promise 闭包中传入 self,它会在 Promise 结束时才会释放,即使 self 指向的对象早就该结束生命周期了。例如:在 VC 中用 Promise 封装一个网络请求,网络请求在 30s 后才到达,然后刷新 VC 的 UI。但如果 VC 在网络响应还未到达前就关掉了页面,按理 VC 相关的逻辑也应相应终止。这时如果 Promise 闭包中传入 self,且 Promise 对象也被 self 或其他生命周期更长对象持有,VC 的生命周期将延长到 Promise 结束时,即网络请求响应到达时。显然这样不符合预期,这时我们才需要在 Promise 闭包使用 [weak self],弱引用捕获 self,这样在网络请求响应到达时,self 不会被 Promise 延长生命周期,就自然终止了 self 相关的方法调用。