PromiseKit 教程

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 对象。提供了 fulfillreject 方法标记成功和失败的结果。

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 对象的时候要对应上,否则出现编译错误。
  • donegettap 原语中的闭包无需返回值;但 thenmaprecover 原语都需在 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 中处理。
  • 要同时处理错误和成功值,使用 ensurefinally 虽然语义类似,但不能接收上一步的结果。
  • 使用 whenrace 处理多个 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 相关的方法调用。

相关推荐
开心就好20251 天前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好20251 天前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao2 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy3 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy3 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode4 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农4 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
iceiceiceice5 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
ssshooter6 天前
Tauri 踩坑 appLink 修改后闪退
前端·ios·rust
二流小码农6 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos