跳至主要内容

PromiseKit 使用教程

 原文链接

PromiseKit 大家都在项目中见过,是典型的一看就会,一用就废的技术。写 PromiseKit 代码想必大家都经历过如下图的错误支配的恐惧。

img

通过本文,让我们深入浅出重新认识 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 使用的难点在于构造和拼接。使用图来表示:

写成代码:

usage-code

这里值得注意的是,由于使用原语连接时是承接上一个 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 相关的方法调用。

评论

此博客中的热门博文

Resolving errSecInternalComponent errors during code signing

原文链接 One code signing issue I commonly see, both here on DevForums and in my Day Job™ with DTS, is that the codesign command fails with errSecInternalComponent. This issue crops up in a wide variety of circumstances and the correct fix depends on the specific problem. This post is my attempt to clarify the potential causes of this error and help folks resolve it. If you have any questions or comments about this, please start a new thread, tagging it with Code Signing so that I see it. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Resolving errSecInternalComponent errors during code signing In some circumstances the codesign command might fail with the error errSecInternalComponent. For example: % codesign -s "Apple Development" "MyTrue" MyTrue: errSecInternalComponent This typically affects folks who are signing code in a nonstandard environm...

iOS:检测使用VPN或Proxy

参考链接: https://www.jianshu.com/p/c3b950dbf86a https://gist.github.com/PramodJoshi/4faad4c91f7dcb4eb9b06be8390c01db http://noodlecode.net/2018/04/check-if-ios-app-is-connected-to-vpn 第一种方法 需要导入框架CFNetwork 然后,这个方法是mrc的:需要添加-fno-objc-arc的flag 代码如下: + ( BOOL )getProxyStatus { NSDictionary *proxySettings = NSMakeCollectable ([( NSDictionary *) CFNetworkCopySystemProxySettings () autorelease]); NSArray *proxies = NSMakeCollectable ([( NSArray *) CFNetworkCopyProxiesForURL (( CFURLRef )[ NSURL URLWithString: @"http://www.google.com" ], ( CFDictionaryRef )proxySettings) autorelease]); NSDictionary *settings = [proxies objectAtIndex: 0 ]; NSLog ( @"host=%@" , [settings objectForKey:( NSString *)kCFProxyHostNameKey]); NSLog ( @"port=%@" , [settings objectForKey:( NSString *)kCFProxyPortNumberKey]); NSLog ( @"type=%@" , [settings objectForKey:( NSString *)kCFProxyTypeKey]); if ([[settings object...

去广告DNS设置,国内ADGuard DNS方案,手机电脑iOS去广告,保护隐私

 原文链接 之前分享过使用mac系统搭建adguard home,这几个月用下来零零散散基本上也被弃用了。主要原因是因为需要保持电脑一直开机。但是我的电脑是笔记本,存在移动各个地域的情况,也就是说只能够屏蔽电脑自身,对于手机而言不太现实。今天偶然发现dnspod推出了高级版的公共解析。dnspod背靠腾讯云,肯定是合法合规的公共解析服务,这个高级版用起来不错。 国内自己搭建解析服务是违法行为,所以这也是为什么使用dnspod的原因。 后台截图 开始使用 首先我们先进入dnspod的公共解析页面,点击开始使用。 专业版公共解析 dnspod会提供几种预设,我们选择「开发者」即可 开发者 然后你就成功的申请到自己个人使用的dns了! 更新拦截规则 我们可以将常见的广告过滤规则加入到dns中。我们在顶部选项卡中选择「拦截规则」。 拦截规则设置 打开adguard adguard 绑定iOS设备 推荐使用描述文件的方式,删除配置时删除描述文件即可。 描述文件 绑定macOS 推荐使用描述文件的方式,删除配置时删除描述文件即可。 描述文件 mac需要在「系统偏好设置」的「网络」中查看是否正在运行。 代理 如果没有运行需要点击「···」来启动服务。 启动服务 绑定路由器 找到自己路由器的DHCP设置,修改dns,然后记得绑定自己的ip。 修改dns 绑定ip 费用 目前有300万次/月的免费额度,但没有超出之后的价格。300万次一个人比较难用完,可以放心使用。 我个人使用iOS设备两台、智能家居、电脑两台,日均请求数大致2万/日。 判断是否搭建成功 可以通过查看日志的方式,日志大概有半小时到一小时的延迟,请耐心等待。