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 相关的方法调用。

相关推荐
/**书香门第*/8 小时前
Laya ios接入goole广告,搭建环境 1
ios
wakangda14 小时前
React Native 集成 iOS 原生功能
react native·ios·cocoa
crasowas1 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store
ii_best2 天前
ios按键精灵脚本开发:ios悬浮窗命令
ios
Code&Ocean2 天前
iOS从Matter的设备认证证书中获取VID和PID
ios·matter·chip
/**书香门第*/2 天前
Laya ios接入goole广告,开始接入 2
ios
恋猫de小郭2 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨3 天前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题3 天前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
二流小码农3 天前
鸿蒙开发:简单了解属性动画
android·ios·harmonyos