Swift中的Futures/Promises原理及用法

这是一篇关于 Swift 中的 Futures/Promises 架构概览,演讲者为我们着重介绍了 FutureKit 的使用方式,从而避免再去调用恼人的 dispatch_async。同时这也是一篇关于异步处理的简要介绍,演讲者讲述了 Futures/Promises 框架是如何帮助我们来实现异步处理的。

通过本次讲演,我们会了解到以下内容:

  • 如何将 Promise 和 Future 互相结合
  • 如何使用 executor 从而让 GCD 更简单
  • 不用再编写带有回调闭包属性的函数了
  • 如何将您喜爱的异步库封装到 Future 当中(例如 Alamofire、ReactiveCocoa 等等)
  • 创建稳定的 (rock solid) 错误处理,以及异常简单的取消操作 (cancellation)

我同样也会对诸如缓存 (caching)、并发执行 (parallel execution)、NSOperationQueues、基于 FIFO 的运行之类的编码模式,来对 Futures 做一个简要的概述。


概述 (0:00)

大家好,我是 Michael Gray。目前我是初创企业 Ambulnz.com 的技术总监。我们现在正在招聘,因此如果您想要在医药运输领域(实际的市场前景超乎您的想象)有一番作为,并且您擅长 iOS 或者 Node 的话,那么您可以来直接和我聊聊。

今天我想要谈论的是 FutureKit 这个框架。它不仅仅是 Futures-Promises 架构的 Swift 实现,并且我觉得它与其他解决方案相比要更为 “Swift 化”。我们将会谈论一下 Future、Promise 等这些术语究竟是什么意思,以及为什么实际上没有人能完全遵守这些观念。

格的不同而已。至于您喜不喜欢这个解决方案,这完全取决于您自己,但是您至少应该了解一下这些解决方案的不同之处。

在我们讨论 Futures/Promises 的概念之前,我们需要先讨论一下代码块 (Block) 和闭包 (Closure)。

代码块与闭包 (2:09)

代 码块与闭包的好处都有啥?它可以让您摆脱那些恼人的委托协议 (delegate protocol)。尽管有些委托协议还是没法摆脱,但是要注意的是,您可以只定义一个带有回调闭包 (callback block) 的函数。比如说在 GCD (Grand Central Dispatcher) 当中,如果您想要在非主线程当中执行某些操作的话,那么您就需要闭包的帮忙。

现在我们要讨论一下这么做的缺点。

func asyncFunction(callback:(Result) -> Void) -> Void {

  dispatch_async(queue) {
    let x = Result("Hello!")
    callback(x)
  }
}

这是一个很常见的例子了。我封装了一个 asyncFunction 方法,从中我创建了某种 Result,但是如果我想要将这个 Result 回调的话,那么我就必须要提供一个回调例程(callback routine)。

如果您经常处理异步事务的话,那么这种代码您此前也一定写过。这里是我所编写的一个例子,无论如何,我们都要进入到一个后台线程 (background queue) 当中,然后在那里生成我们所要的 Result,接着将 Result 回调回去。

回调闭包的问题所在 (3:42)

事实上,我将会讨论一下 AFNetworking 的相关设计。我觉得之所以 AFNetworking 能够成为 iOS 开发者首选的第三方库,是因为它摈弃了存在于 NSURL 当中的那些可怕的委托协议。它是基于回调进行的。

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
  .response { request, response, data, error in
    print(request)
    print(response)
    print(data)
    print(error)
}

这是 Alamofire 的一个例子,是它最新版本的一个示例。我在这里不会讲解 Alamofire 的相关知识。虽然我不喜欢 Alamofire 这个第三方库,但是我觉得以它为例来介绍回调闭包是再好不过的了,我们提出了一个网络请求以获取相关数据。这里我将会获取到某种网络回应 (response) 对象,并且您可以看到这些选项——requestresponsedata 以及 error——这些选项都是可能在这个异步 API 调用的时候出现的。

好的,大家可能会问了,这看起来挺正常的啊,怎么会有问题呢?实则不然,当您运行了某个带有回调闭包的函数时,就会出现很诡异的现象。举个例子,这个闭包将在何处运行呢?

一 旦您开始编写异步代码,这就意味着这段代码将不能在主线程当中运行,因为如果您将这段代码写到了主线程上,那么您的应用会卡顿在那儿,因此您必须要将这些 代码放到后台来执行。因此,当您运行一个本身就在后台运行的回调函数时,按正常道理来说它随后会将结果回调给您,但是问题来了,这个回调的结果在哪里呢?这个回调闭包现在是否必须要派发 (dispatch) 到主线程上呢?但是如果这个回调我仍然不希望它在主线程上运行呢?

另 外的问题是,我们该如何进行错误处理呢?我是不是要实现两个回调?我看到过有 API 提供了一个成功时调用的回调代码,以及一个失败时调用的回调代码。我用一个例子给大家演示一下,为什么绝大多数人会使用 Swift 的枚举来规避这个情况的发生,当然如果您这么做也是很好的一个解决方案。但是这仍然存在一个问题,您该如何界定错误的范围呢?

此外就是取消操作 (cancellation) 了。多年以来,对于这些第三方库来说有这么一个经久未决的问题:那就是如果我有一个操作,然后我取消了这个操作,那么是否需要调用取消的回调呢?以及,我该如何处理这个取消操作呢?

func getMyDataObject(url: NSURL,callback:(object:MyCoreDataObject?,error: ErrorType) ->
Void) {
  Alamofire.request(.GET, url, parameters: ["foo": "bar"]).responseJSON { response in
    switch response.result {
    case .Success:
      managedContext.performBlock {
        let myObject = MyCoreDataObject(entity: entity!,
          insertIntoManagedObjectContext: managedContext)
        do {
          try myObject.addData(response)
          try managedContext.save()
          dispatch_async(dispatch_get_main_queue(), {
            callback(object: myObject, error: nil)
          })
        } catch let error {
          callback(object: nil,error: error)
        }
      }
    case .Failure(let error):
      callback(object: nil,error: error)
    }
  }
}

这是一个典型的获取数据的函数,用标准的 GCD 回调写成。我执行了 Alamofire 网络请求,准备去 GET 数据。然后我得到了相应的网络回应,它是以枚举的形式返回,这是一个很好的范例。

再提及一遍,之所以给大家展示 Alamofire 的原因不是因为我喜欢它,而是我觉得它在使用回调方面无出其右,但是我们还是来看一下它的缺点所在。

现在我们获取到了 response 对象。如果成功的话,呃,这是一个 API 调用,然后我想要从服务器那里返回一个模型对象。我们使用这个 URL 来进行 Alamofire 的网络访问,假定我得到的网络回应是成功的。

这时候,我会创建一个相应的管理对象上下文 (managed object context),然后对其执行 performBlock,由于管理对象上下文处于不同的上下文当中,并且我还要确保我没有在主线程执行这个方法。因此我需要使用后台进程。

这时候让我们去调用这个 performBlock。现在我位于另一个线程当中了,所以也就是说,我现在是很安全的,这个时候我们就来构造 MyCoreDataObject,然后使用这个 addData 方法,这个方法是我自行编写的,它可能会抛出一个错误。

这里我们就要借助 Swift 的错误处理机制来实现错误处理了,因为可能我访问的服务器并没有返回合法的 JSON 给我。之后就是尝试将更改操作保存到 Core Data 里面,这个操作也可能会抛出错误。

最后,由于我知道当前我正位于后台线程当中,因此我不希望在 Core Data 的队列当中返回我想要的结果,因此我需要在主线程执行这个回调。

通过这样,将使得其他人在调用这个 API 时不会犯太多的错。它们可以直接在回调中修改界面,以及执行其他适合在主线程当中完成的事情。因此我会在 dispatch_async 当中完成这段操作。如果发生了错误,那么我就必须要返回一个错误类型。

实际上,您可以很清楚地看到,在这其中出现了三次嵌套 (interaction) 的回调。这些回调甚至还有可能会嵌套到五次、六次,而这则会导致更糟糕的事情发生。

为什么要使用 Futures/Promises (7:50)

那么什么是 Futures/Promises 呢,为什么人们要使用它们呢?基本上,它们可以帮您摆脱回调的困扰。

我们使用过大量的 JavaScript Promises,它们用起来真的很棒。那么 Future 和 Promise 有什么区别呢?毕竟它们都是用来让您摆脱回调的困扰的,这是它们最主要的作用。

那 么这两个词到底什么意思呢,我该什么时候用 Future,什么时候用 Promise 呢?在我见过任何一种实现中,这两者都不一致。JavaScript 只是将事件称之为 Promise,但是 JavaScript 是动态类型的,所以使用起来比较单。此外也有只使用 Future 的相关实现,因为在理论中,所有的操作都可以在一个单独的对象当中完成。

我 从 Scala 那里窃取来了 FutureKit 的实现。再次强调一点,这只是一种选择,因为这些实现方案基本上是非常相似的。对于 FutureKit 来说,我们在用户界面当中需要使用 Future 对象。这意味着您的函数很可能需要返回一个 Future 对象。

如 果您正在定义诸如函数之类的东西,那么您所提供给用户的 API 需要返回一个 Future 对象,而 Promise 则更为底层,它是用来生成 Future 的东西。我喜欢 Promise 这个词语,因为当您创建一个 Promise 的时候,您必须要信守诺言 (keep promise)。如果您创建了一个 Promise,并返回了一个 Future,那么您就必须要完成这个操作,否则的话您的代码就会异常终止。

func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> {
  let executor = Executor.ManagedObjectContext(managedContext)

  Alamofire.request(.GET, url, parameters: ["foo": "bar"])
    .FutureJSONObject()
    .onSuccess(executor) { response -> MyCoreDataObject in
      let myObject = MyCoreDataObject(entity: entity!,
        insertIntoManagedObjectContext: managedContext)
      try myObject.addData(response)
      try managedContext.save()
      return MyCoreDataObject
    }
  }
}

这段代码和我们之前所看到的那一段代码在功能上是一样的。不同的是,这是用 FutureKit 所实现的。

首先,您会看到顶部的函数结构非常不一样,现在回调已经不存在了。它现在只是,接收一个我需要获取数据的 URL 地址,然后返回一个 Future<MyCoreDataObject>。注意到 MyCoreDataObject 的可选值已经不存在了。

随后我在这里创建了一个 executor。这是另一个我基于 ManagedObjectContext 所创建的 FutureKit 产物。如果您使用过 Core Data 的话,就会明白,在这里它为您封装好了 performBlock

好的,现在我执行了 Alamofire 的网络请求,这个 FutureJSONObject 是 FutureKit 对 Alamofire 的一个扩展,它可以将 response 对象从 Alamofire 的形式转换为 Future。

现在我就可以说了,onSuccess(executor) 意味着,后面的这个闭包就是在 performBlock 当中要运行的那段代码。如果您注意到这里的架构的话,就会发现我可以获取到我所获取的 response 对象,然后返回 Core Data 对象即可。

随后就是 try 操作了。注意到我们没有添加任何的 do 或者 catch。这是因为这些错误处理已经由 handler 自行帮我们处理好了。因此现在我就可以直接使用 try 来添加数据、保存数据,然后将对象返回即可。

这里的关键在于,这个函数要返回一个名为 Future 的对象,Future 当中拥有许多 handler 来处理里面的内容。

最主要的 handler: onComplete (11:30)

在 FutureKit 当中最主要的 handler 当属 onComeplete 了。如果您切实了解了 onComplete handler,那么您就能明白 FutureKit 当中的其他 handler 是如何工作的了。您会看到,其他的 handler 都是基于这个 onComplete 的便利封装而已。

let newFuture = sampleFuture.onComplete { (result: FutureResult<Int>) ->
Completion<String> in
  switch result {

  case let .Success(value):
    return .Success(String(value))

  case let .Fail(err):
    return .Fail(err)

  case .Cancelled:
    return .Cancelled
  }
}

我调用了 Future 对象的 onComplete 方法,然后得到了这个名为 FutureResult 的枚举对象。再次强调一下,这个对象是一个泛型,因此这里我们得到的一个包含某种整数值的 FutureResult,然后最后我们会返回一个 Completion 对象。现在我来具体说明一下这两个对象。

public enum FutureResult<T> {
  case Success(T)
  case Fail(ErrorType)
  case Cancelled
}

首先第一个是 FutureResult。这基本上是每个 Future 结束的时候都会生成的玩意儿。

与其他 Future 的实现所不同的是,FutureKit 增加了一个名为 Cancelled 的枚举。我们来详细解释一下这个 Cancelled,为什么要把它单独提取出来,而不是放到 Success 或者 Fail 当中,而这种做法往往是其他异步实现方案所做的。

public enum Completion<T> {
  case Success(T)
  case Fail(ErrorType)
  case Cancelled
  case CompleteUsing(Future<T>)
}

这是另一个名为 Completion 的枚举。Completion 似乎很容易让人困惑,但是实际上它很好理解。

最简单的方式就是将 Completion 视为结束 Future 的一项操作。您可以将其视为另一个 Promise,这样它看起来就有点像是 FutureResult,但是它拥有了额外的枚举值:我希望先完成括号里面的 Promise,然后再来完成当前的这个 Promise。这样当我们开始组合 Promise 的时候,它就非常有用了。在这个 onComplete 当中,您实际上可以看到这样的做法。

case let .Success(value):
    return .Success(String(value))

  case let .Fail(err):
    return .Fail(err)

这里实际上有两个不同的枚举。第一个是 FutureResult,第二个是 Completion。

绝大多数时候,您都不会去调用 onComplete。您只需要在有特殊情况的时候才去调用它。大多数人基本上都只会去调用 onSuccess

let asyncFuture5 = Future(.Background) { () -> Int in
  return 5
}
let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in
  let five = value
  print("\(five)")
  return five
}

这是一个很典型的例子,它展示了在 FutureKit 当中我们该如何使用这个 onSuccess。首先我在第一行创建了一个 Future 对象。这是创建 Future 最轻松、最简单的方法了。这里使用了 .Background,随后我们会对其进行更深的讲解,这是一个很有意思的 executor。

我 这里的做法是在后台当中创建一个 Future 对象,我们这里所做的就是生成这个数字 5。假定出于某种特殊的原因,执行这项操作会占用大量的时间,因此我希望在后台完成这项操作,所以我使用这种方法来生成 Future 对象。它实际上会给我们返回一个数字 5,好的现在让我们来看看这个 onSuccess

现在我可以说,我需要确保 onSuccess 在主线程上运行,因为我需要执行某种 I/O 操作。我获取 Future 所返回的值,然后将其打印出来。好的现在这个时候,value 实际上是一个值类型,而不是一个枚举了,也就是说,它已经是我们所期望的值了。

let stringFuture = Future<String>(success: "5")

stringFuture
    .onSuccess { (stringResult:String) -> Int in
        let i = Int(stringResult)!
        return i
    }
    .map { intResult -> [Int] in
        let array = [intResult]
        return array
    }
    .onSuccess { arrayResult -> Int? in
        return arrayResult.first!
    }

Future 不仅能够执行 onSuccess,而且还可以将其映射为一个新的 Future 对象。

假设我创建了这样一个 stringFuture,我是从一个函数当中所获取到的。这里我们用了一个简便的方法,来创建一个已完成的 Future 对象。这个 Future 对象会返回一个字符串,并且它已经成功结束了。

接着,在第一个闭包当中,我使用了 onSuccess,我需要将字符串转换为 Int 类型。Swift 编译器的类型推断非常好用,它会自行知道下个 map 当中实际上会是什么类型,当然您也可以将 onSuccess 称之为 map,因为这两者的行为非常相似。

现在我会将我所得到的结果映射为 Int 数组。基本上,您可以将任何一种 Future 转换为另一种 Future。您会注意到我们这里的语法非常简明,如果您写过很多函数式和反应式代码的话,那么这种风格您一定不会陌生。因为这些风格非常相似,虽然有所不同,但是都非常简洁、美观、没有任何回调。

func coolFunctionThatAddsOneInBackground(num : Int) -> Future<Int> {
  // let's dispatch this to the low priority background queue
  return Future(.Background) { () -> Int in
    let ret = num + 1
    return ret
  }
}

let stringFuture = Future<Int>(success: 5)

stringFuture
  .onSuccess { intResult -> Future<Int> in
      return coolFunctionThatAddsOneInBackground(intResult)
  }
  .onSuccess {
      return coolFunctionThatAddsOneInBackground($0)
  }

这就是 Future 所带来的好处了,它可以使得用户界面变得高度可组合 (highly-composable)。这里我给大家展示一个稍微详细一点的示例。

我创建了这个可以在后台执行数字运算的函数。这个函数需要在后台运行,然后将传递进去的数字加一,至于这样做的理由,是出于某种原因我们不希望在主线程运行它。现在,如果您注意到的话,我创建了这个 stringFuture,这个 stringFuture 和之前的相同,但是我要做的是返回一个新的 Future,而不是返回一个新的数值。因此我们可以使用 map,将这个值映射到另一个 Future。

Futures/Promises 让代码变得易于组合。所有的 Future、Promise 的实现基本上都是由这些基本结构所组成的。这个特性非常讨人喜欢。

什么是 Promise 呢? (16:47)

那么什么是 Promise 呢?假设您需要在某个地方去创建一个 Future 对象。我创建了一个 Promise,这个 Promise 会返回一个字符串数组。随后,当我调用 completeWithSuccess 之后,我就可以得到这个真正的字符串数组了。

“`swift

let namesPromise = Promise<[String]>()

let names = [“Skyler”,”David”,”Jess”]
namesPromise.completeWithSuccess(names)

let namesFuture :Future<[String]> = namesPromise.Future

namesFuture.onSuccess(.Main) { (names : [String]) -> Void in
for name in names {
print(“Happy Future Day (name)!”)
}
}

这意思是说,我实现了这个 Promise,如果 Promise 允诺了,那么我们就得到了结果。您会注意到,所有的 Promise 都包含这样一个名为 `.Future` 的成员。这就是您可以返回的 Future 对象,这是用来让 Promise 操作用户界面的方式之一。这样我现在就可以来实际执行这个 Promise。

```swift

func getCoolCatPic(catUrl: NSURL) -> Future<UIImage> {

  let catPicturePromise = Promise<UIImage>()

  let task = NSURLSession.sharedSession().dataTaskWithURL(catUrl) { (data, response, error) ->
Void in
    if let e = error {
      catPicturePromise.completeWithFail(e)
    }
    else {
      if let d = data, image = UIImage(data: d) {
        catPicturePromise.completeWithSuccess(image)
      }
      else {
        catPicturePromise.completeWithErrorMessage("didn't understand response from \
(response)")
      }
    }
  }
  task.resume()
  
  // return the Promise's Future.
  return catPicturePromise.Future
}

通常情况下,当您需要 Promise 的时候,这就意味着您需要封装某种接收回调为参数的东西。如果您遇到了某个使用了回调的第三方库的话,那么你可以借助 Promise 将它们 Future 化,以将它们转换成 Future 的形式。

这 里我举了一个很典型的例子,我是从 FutureKit 当中的 Playground 取出来的例子,这段代码的作用是获取一些可爱的猫咪照片。在幻灯片里面效果不是很好,因为在 Playground 当中,您可以切实看到猫咪的图片,但是这里你只能想象这段函数能够获取到猫咪的图片。

我需要这样一个函数,给定 URL,然后返回图片给我。仔细想一下,为了获取这个图片对象,我不仅需要访问网络,从我的网络层那里将数据对象拉取下来,还需要将这个数据对象转换为 UIImage,然后再将其返回。所有的这些步骤都已经封装在这个函数里面了。

我将准备使用标准的 NSURLSession。它接收我的 URL 为参数,然后现在我完成了这个回调 response 的设置。我从中拿取到了我需要的 data、response 和 error 对象,然后我就可以开始对其进行解析了。

如 果我接收到了错误,那么我就需要让我的 Promise 以失败告终。如果我接收到了 data,并且还可以将其转换成为图片,那么我的 Promise 就是成功的。如果这个操作失败了,并且我也不想创建一个自定义的错误类型的话,那么我就使用这个内置的错误方法,提示“我无法识别这个 response”。我们以这个 NSURLSession 开始,然后以 Future 结束。

如果您对它运行的方式有疑问的话,其实这整个内部的闭包随后才会运行,但是这个 Future 会被立即返回。您可以知道这个 Future 当中封装了哪些内容。

如何进行错误处理呢? (19:23)

现在让我们来看一下错误和错误处理。这正是我觉得 Future 的妙处所在,因为这样只要您的步骤正确,那么您就不用考虑潜在的问题了,Future 的错误处理非常好用。

当我们在处理回调的时候,对于每个封装的单独 API 回调来说,我们都必须要处理错误情况。您必须要大量地检查错误。我不喜欢在回调中大量添加错误处理,因为它们会让回调变得更加复杂、更加糟糕。

例 如,假设您准备调用某个 API。然后突然发生了网络问题。假设我正从服务器获取一个 JSON 对象,因此有可能是我的 JSON 解析发生了错误,也有可能是验证出现了问题。好的现在我要对这些问题进行处理了,一旦我成功对对象进行了解析,那么我就需要将其存储在数据库当中。然后又 发生了文件 I/O 的问题,或者也有可能是数据库内部数据验证的问题。

这些问题都很有可能会发生。我希望能够给调用者提供一个高度封装的 API,它只执行某一件事情,而其他的错误则不必去关心。在 Future 的世界里,这意味着如果您的操作一切正确,那么 Future 将会输出正确的结果。

FutureKit 并不会让您去定义特定错误类型的 Future。当您尝试去创建特定错误类型的 Future 时,它实际上破坏了架构的可组合性 (composability),正如您前面所看到的那样。因为这样的话您就必须要将所有的组合进行过滤,不停地进行 map 转换以分别处理不同的错误情况。您可能会觉得这种做法挺好的,但实际上它使得代码变得更糟了。

另一个关于 FutureKit 的是,由于它没有特定错误类型,因此您会注意到它同样也没有 ErrorType。 有人总会建议您去创建一个不会返回 Error 的 Future,但是我们发现,这种做法实际上还是破坏了 Future 的可组合性。对于异步来说,它的核心思想在于这个操作有可能不会成功。就算现在不会出错,那么将来的某一天还是可能会出错。那么这种类型的 Future 来说,它们只能在没有错误发生的条件下才能正常工作。

那么让我们来看一下 FutureKit 是怎么做的,如果您创建了一条 Future 调用链,但是您忘记在末尾加上错误处理的话,那么您会得到一个编译警告。它会告诉您:“您调用了 Future,但是我没有发现您对错误有任何的处理。”

关于 FutureKit 的另一个好处在于,您所看到的这些 handler:onCompleteonSuccessonFail。它们都内置了 catch。因此您就不必再用 catch 或者 do 去包装这些方法了。如果您调用的 Swift 方法需要使用 try 的话,那么不必担心。直接加上 try 即可,Future 在内部已经自行帮您完成了基础的错误处理了。

func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> {
  let executor = Executor.ManagedObjectContext(managedContext)

  Alamofire.request(.GET, url)
    .FutureJSONObject()
    .onSuccess(executor) { response -> MyCoreDataObject in
      let myObject = MyCoreDataObject(entity: entity,
              insertIntoManagedObjectContext: managedContext)
      try myObject.addData(response)
      try managedContext.save()
      return MyCoreDataObject
    }.onFail { error in
      // alert user of error!
  }
}

现在,您就可以看到这个好用的 onFail handler 了。我回到我之前的例子当中,然后添加了这个 onFail handler,因为如果我对老代码进行编译的话,那么我会得到一条编译警告。现在我加上这条之后,就没有任何副作用了。

在 FutureKit 当中,还有一点和其他解决方案不同。onFail handler 并不会去干涉错误。这和 JavaScript 不同,您会选择去调用 catch,因为这样您就不用去理会错误了。但是实际上 catch 仍会对错误有所干涉,也就是说如果您如果在函数中使用了 catch 的话,那么人们很有可能会忘记您实际上对这个错误添加了某些副作用,如果不对这个错误进行处理,那么程序就很可能会发生崩溃。

FutureKit 强制要求需要对错误编写 handler,因为您写的函数是异步的,我们都知道,异步操作很有可能会失败。onFail 的目的在于处理错误,而不是捕获错误。因此您没必要将错误进行传递;您必须要使用 FutureKit 中的 onComplete

虽然这可能只是所谓的习惯问题,因为 onFail 实际上已经可以执行很多操作了,但是我对我手下的开发者们并不信任,它们不一定会对去写 onFail

现在您知道,当您在使用 onComplete 的时候,有人可能会将 onComplete 的涵义混淆起来,例如将一个失败的 Future 转换为一个成功的 Future。这种情况只占十分之一。其余的时候,如果 Future 失败了,您可能只是希望能够将中间数据清理掉,然后通知用户即可。

取消操作 (Cancellation) (24:21)

好 的,另一个您会在 FutureKit 当中看到的重要东西就是取消操作了。这是每个异步操作都可以使用的操作。当您开始一个异步操作的时候,您或许会意识到这个操作的结果是不需要的。您不希望 那些 handler 被运行,因为这些 handler 已经毫无意义了。举个例子,您打开了某个视图,然后开始从网络上提取需要的数据,然后接着您点击了返回,关闭了这个视图控制器。而这个时候这个异步操作仍 然还在运行,这个时候我们需要将其清除掉。

现在,我们可以添加 onCancel 操作了。当我需要知道某个异步操作有没有被取消的时候,我们通常使用 onComplete 来完成。FutureKit 的取消操作非常好用。如果您看过 Github 上的源码的话,您会发现虽然代码不是很长,但是要明白它实际的运行原理还是非常困难的。

现在让我们看一下这个会返回 Future 的函数:

let f = asyncFunc1().onSuccess {
  return asyncFunc2()
}
let token = f.getCancelToken()
token.cancel()

这个函数已经和一个 onSuccess 组合在了一起,它接着会返回第二个 Future。问题是,如果我在上面调用了 onCancel,那么我是取消了 asyncFunc1 还是取消了 asyncFunc2 呢?

实 际上这两个函数都会被取消掉。它会先取消第一个函数,然后如果第一个已经完成了,那么您也不必担心,它会取消第二个函数。如果您需要实现取消操作的话,那 么很简单。在 Promise 上有一个 handler,它可以标明需要一个可取消的 Future。当您被告知 Future 被取消之后,您就需要对相关内容进行清除操作了。

let p = Promise<Void>()

p.onRequestCancel { (options) ->
CancelRequestResponse<Void> in
  // start cancelling
  return .Continue
}

这实际上是一个枚举的 response,您既可以声明您暂时还不想要取消 Future,因为需要等待清理操作完成,或者也可以直接取消 Future。

public extension AlamoFire.Request {
  public func Future<T: ResponseSerializerType>(responseSerializer s: T) -> Future<T.SerializedObject> {
    let p = Promise<T.SerializedObject>()
    p.onRequestCancel { _ in
      self.cancel()
      return .Continue
    }
    self.response(queue: nil, responseSerializer: s) { response -> Void in
      switch response.result {
      case let .Success(t):
        p.completeWithSuccess(t)
      case let .Failure(error):
        let e = error as NSError
        if (e.domain == NSURLErrorDomain) && (e.code == NSURLErrorCancelled) {
          p.completeWithCancel()
        }
        else {
          p.completeWithFail(error)
        }
      }
    }
    return p.Future
  }
}

您可以在 handler 中同时完成两个操作。我回到之前我用 FutureKit 对 Alamofire 做的扩展封装示例当中。我在这里对 Alamofire 序列化对象进行操作。这个函数是个泛型函数,允许我将 Alamofire 序列化对象转换为 Future。

第一个事情是为试图序列化的对象创建一个 Promise,然后为其添加取消 handler。

如果我需要执行取消操作的话,那么我就会调用这个 self.cancel,这是内置的 Alamofire 取消请求方法,然后如果需要继续执行的话。那么接下来您会看到在这里,我对错误结果进行了处理,如果我发现错误是用户取消操作的话,那么我就让其 completeWithCancel

新的 Swift 反面模式 (anti-pattern) (27:39)

一旦您理解了 Future 的原理,那么当您再去看自己的代码时,就会意识到原来的代码已经变成了一种全新的反面教材。当您看到别人的代码时,您会坐立不安:“请一定不要继续这么下去了!“

其 中一个反面模式就是那些带有回调属性的函数。当您看到它们的时候,就意味着麻烦来临了。另一个反面模式就是,我注意到,当初学者开始使用 Future 的时候,他们通常都会创建带有可选值结果的 Future。一般而言,这是不必要的。我们一般情况下是不会需要可选值的存在的。

因为在回调中,之所以回调方法需要接受可选值作为参数,是因为所得到的不一定是实际结果,也有可能得到错误。如果没法得到预期结果的话,那么就说明这个过程肯定是失败了,这才是正确的做法。

还有一件非常重要的事,这可以让您写出优秀的 Future 实现,就是如果我需要能够运行在后台的操作的话,那么我们应该让函数自身来定义自己的运行环境上下文 (execution context)。

因此,对于我的这个图像库而言,我不希望它会在主线程上运行,而且我也不用让调用 API 的人员来操心这件事。我只需要让函数自身前往到合适的线程上去运行即可。我们接下来会谈论一下这是如何工作的。

Executors (29:07)

FutureKit 当中,另一块重要的部分就是 executor 了。在 Future 其他语言的实现当中,它的名字可能会被命名为 ExecutionContext。这是受到了 Bolts Executors 的启发。

FutureKit 的 executor 实际上是一个枚举。正如您所注意到的,FutureKit 非常喜欢使用枚举。FutureKit 当中的 Executor 类所有的枚举值都与内置的 GCD 建立了镜像关系。绝大多数时间,您都只是使用内置的 GCD。很快,我就收到了反映这些名字的一系列数字。

let asyncFuture5 = Future(.Background) { () -> Int in
  return 5
}
let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in
  let five = value
  print("\(five)")
  return five
}

这就是我觉得 FutureKit 设计得非常好的地方。我创建了一个 Future,然后我希望它能够在后台运行,然后我运行了它。也就是用这个 onSuccess 命令来执行。我可以在这里添加一个 executor。

实际上,executor 的种类有很多种。不过有一些是比较特殊的。首先是 .Immediate executor。这个即时 executor 意味着,我不在乎这个代码块在何处运行。它可以在任何地方运行。通常而言这是最常用、最有效率的 executor 了。我们通常而言都会直接使用这个 executor。比如说我们这里接收一个整数为参数,然后将其转换成字符串,而它运行的地方我们并不在乎。因此我们不必重新调整线程。

此外还有 .MainImmediate,这意味着我希望代码块在主线程运行,但是如果代码已经在主线程上运行了,那么就不用对线程进行调整了。如果没有的话,那么就将线程调整到主线程。还有 .MainAsync,它意味着代码始终需要异步派遣 (async dispatch)。比如说某个委托非常奇怪,您必须要让委托结束运行,但是你还需要这段代码能够运行。

然后还有一些 executor,比如说您已经创建了自己的派遣队列,那么您可以将这个队列封装到一个 executor 当中。您也可以将 NSOperationQueue 封装到 executor 当中。如果您还需要处理闭包操作的话,您还可以将管理对象上下文 (managed object context) 封装到 executor 当中。

然后,还有一些非常智能的 executor。我毕竟倾向于使用这些 executor,因为它们实际上将多个 executor 的行为整合在了一起。您可以在代码当中声明它们,然后它们会在运行时映射为实际需要的 executor。最好用的一个就是 .Current 了。如果您已经使用了一个 executor 了,那么它会接着使用这个 executor。因为事实证明,对于程序员来说,因为您已经使用了一个 executor 了,一般那么就没必要再去创建一个新的了。代码一旦在后台执行了,那么就说明这段代码会一直想留在后台运行。

好的,如 果您在某个 executor 当中封装了自己的执行操作,或者并没有给 FutureKit 定一个 executor 的话,那么它会自行去推断合适的 executor。是谁在调用这段代码呢?这段代码位于何种运行操作上下文当中?我会确保这个闭包会自行进入到运行操作上下文档中,即使内部的方法决定需 要去后台执行,它也会一直如此。

下一个 executor 就是 .Primary 了。所谓的 .Primary 就是指那种您没有告知 FutureKit 您正在使用的是何种 executor,也没有说明您需要何种 executor,这是 executor 的默认值。最后四个都是可配置的,它们都是某种 FutureKit 的操作。

我倾向于使用的 executor 是这个 .Async,这意味着,我需要前往后台的某处来运行这段代码,或许随后我会决定要不要为默认操作改变 QoS。

最后就是这个 .Custom 了,.Custom 允许您构建属于自己的 executor。举个示例,假设我有这样一个所需要的操作,它需要在主线程当中运行,但是我可能需要在后台队列当中花点时间来处理这段操作,因为这个操作并不是很重要。

我们来举个比较怪异的例子吧,我们可以创建一个 executor,它会在后台执行,然后等待某个事件完成之后,又重新回到主线程来执行。

let ex: Executor = .Background

let f = ex.execute {
  let data = expensiveToComputeData()
  return data
}

executor 同样也有很好用的执行方法,这也是一种生成 Future 的简单方式。您在这里可以注意到,我创建了一个在后台运行的 executor,然后我调用了它的 execute 方法。我调用了某个方法,这个方法用来获取数据,但是非常地耗时间,然后我需要将这个数据返回。此外,还有一些带有延迟和额外事件的 execute 方法。

let shiftFuture = model.getShift()
let vehicleFuture = model.getVehicle()

combineFutures(shiftFuture, vehicleFuture)
  .onSuccess { (shift,vehicle) -> Void in
    print("\(shift),\(vehicle)")
}

接下来,我们要做的就是将 Future 联合起来。在这个例子当中,我们需要并发执行。当我们有一系列需要一起运行的操作时,我们并不希望让它们一个个地运行。我们想要让这些操作全部立刻执行, 因为您可能实在进行某种配置操作。让我们来运行以下这段代码,没有理由我们必须要将它们序列化。

combineFutures 是一个非常好用的、类型安全的函数,它接收两个 Future 为参数,然后会创建一个带有双元结果的新 Future。或许我有某种能够生成 Shift 模型对象或者 Vehicle 模型对象的模型,现在我将准备把这两个 Future 组合成一个单独的 Future。当两个操作都成功之后,它将会把结果返回给我。再强调一遍,只要任意一个 Future 失败了,那么整个 Future 集都将失败。

public protocol CompletionType {
  associatedtype T
  var completion : Completion<T> { get }
}

我想要再深入地谈一下 FutureKit 所做的东西。这个 CompletionType 协议是一个更高级的玩意儿。这是 FutureKit 的一个扩展,它可以让您自行建立想要的东西,它已经是异步化的了。

或 许您想要将 Alamofire 的网络请求改造成一个 Future,那么您可以使用这个协议对其进行扩展,然后将其转换成相应的类型,这样 FutureKit 就可以自行将其翻译,最后生成一个 Future。因此,这个新的请求就可以被加到 Future 的 handler 当中,这确实非常方便。

public protocol ErrorTypeMightBeCancellation : ErrorType {
    var isCancellation : Bool { get }
}

这里有一个问题,我之前说过,取消操作并不是错误。但是如果您的某个库将取消操作视作错误的话,并且您还希望将这个错误转变为对应的取消操作,那么您可以使用这个 ErrorTypeMightBeCancellation 对错误进行扩展。之后您就可以计算 isCancellation 属性是否为真。只要为真,一旦这个错误在 handler 当中发生,那么 FutureKit 会自行将它转换为取消操作。

进阶事项及结论 (36:11)

既然您对这些基础知识已经有所了解了,那么我们下面就来聊一聊进阶事项吧!我不会去将这些第三方库的实现细节,但是我会给大家大概讲解一遍。

NSCache.findOrFetch(key, expireTime:NSDate? = nil,
onFetch: () -> Future<T>)

为 Future 建立缓存是我一直想要做的事情。之所以这么做,是因为当您进行了一个时间耗费长的操作时,那么这项操作就必须异步进行,然后当该操作完成之后,您就可以将 这个操作的结果缓存起来,这样就不用再次去执行这个操作了。比如说图像缓存之类的时间耗费长的操作就很适合进行缓存。

但是缓存同样也会带 来不好的问题,因为当这个异步操作开始的时候,只有当其成功结束之后才能够保存到缓存当中。当您用多了 Future 或者之类的异步后台运行代码的时候,您会发现很可能会有多个地方都需要执行这个操作,那么在第一个操作结束缓存之前,那么这些地方都仍将继续执行异步操 作。

我们可以通过一点小小的技巧来规避这个问题,这就要用到 NSCache 上的一个既有的扩展了。也就是 findOrFetch 函数,它允许您通过定义键来寻找缓存,如果查找失败,那么您就可以返回一个方法,这个方法返回一个 Future,接着 NSCache 就会去进行检索。接下来,如果其余的请求命中了这块缓存的话,那么它们所得到的都将是同一个 Future。它们都将获取同一个结果,这样您就可以加快这个异步请求的速度了。

此外,还有一个名为 FutureBatch 的对象。当 combineFutures 力不从心的时候,尤其是您拿到了一个未知类型的 Future 数组并且也不知道里面有多少个 Future 的时候,FutureBatch 就派上用场了。此外,当其中的某个子 Future 失败的时候,如果您还需要进行很多控制和决定,比如说决定整个批次的 Future 是否都会全部失败,还是仅失败的那个子 Future 失败?如果您需要得到每个子 Future 的独立结果,那么请使用 FutureBatch

此外还有 FutureFIFO,麻雀虽小,五脏俱全。

let fifo: FutureFIFO()
let f:Future<UIImage> =
  fifo.add {
    return functionThatBuildsUImage()
}

另一件严肃的问题是,当您在撰写大量的异步代码时,您会意识到您需要进行序列化操作。FutureFIFO 的特性是先进先出。它为您提供了一个 add 方法,您只需要重新返回一个 Future 即可。它确保这些要允许的代码块,在上一个代码块完成其 Future 之前,都不会运行。

这和队列分割有所不同,队列分割会确保代码块一定会运行。这里是逻辑上的异步队列。最好的例子就是下面这个经典调用:

首 先,我要去与服务器通信。这时候我进行了某种 REST 操作,我要确保我能够执行 API 调用,并且还能够得到返回的 JSON 数据,之后我要去修改数据库,然后将数据写入到数据库当中,最后将结果返回,然后我就不想要进行下一个 API 调用了。我希望下次能够直接从数据库当中读取这个已写入好的数据。

但是由于每次调用都位于不同的异步队列当中,这意味着所有的操作都会被执行。在这儿,您可以将其推入到 FIFO 队列当中,这样在前一个操作完成之前,后面的模型操作都不会进行。

NSOperationQueue.add<T>(block: () throws -> (Future<T>)) ->
Future<T>

let opQueue: NSOperationQueue
let f:Future<UIImage> =
  opQueue.add {
    return functionThatBuildsUImage()
}

NSOperation 写起来很烦人,但是当您使用诸如 Future 之类的实现方式的时候,那么写起来就会很容易了。

让我们说一说我的想法,我不想使用 FIFO,也不是对并行层级进行控制。假设,我有一个图像操作的批处理集,并且我知道在我的 iPad 上有三个核心,然后我想要同时运行两个图像操作。我或许会使用 NSOperationQueue,将兵法层级设定为 2,然后我就可以添加运行这些代码块的方法,从而在 NSOperationQueue 内部运行。

现在我们使用 Future 来干掉这些恼人的委托。如果大家看过这些委托代码的话,就会意识到委托是异步操作当中最可怕的东西之一,它会将您的 iOS 或者 macOS 代码变得杂乱不清的毛线团,因为代码逻辑将会缠绕在一起,很难分离。

有 些时候人们甚至还会与视图控制器进行交互,当视图控制器完成之后,就会有某种回调的委托来通知您结果如何。我们要做的就是给视图控制器添加一个属性,如果 您想要知道用户何时选择了某样东西的话,那么您需要创建一个 Future。如果要进行操作的东西有很多不同的输出的话,那么我们可以创建一个枚举值。用户选取之后,就会生成 Future 结果。

public extension SignalProducerType {

public func mapFuture<U>(transform: Value ->
Future<U>?) -> SignalProducer<U?, Error>
}

关于 Future 和 Reactive 之间的区别,有很多人问了我这个问题了。但事实上,Future 和 Reactive 可以很好的协同工作,只要您了解何时使用 Future,何时使用 Reactive 即可。

我 发现很多人使用 Reactive 代码来完成本该是 Future 所做的事,因为他们知道这些工作该如何在 Reactive 当中进行。假设我有一个会随时变化的信号量,并且我想要进行监听,那么您就需要使用 Reactive 了,例如 RxSwift 或者 ReactiveCocoa。

如果您只是到后台当中执行操作,然后返回一个简单的结果,最后就把信号量给关闭掉,那么您应该使用 Future 而不是 Reactive,因为 FutureKit 当中的可组合性正是您所希望的。

如果您尝试将这些异步第三方库当中的异步操作给组合起来,您会发现这会非常、非常困难。而使用 Future 的话,你只需要匹配和回取数据,然后在后台执行,最后就可以得到一个结果,万事大吉。

问答时间到! (40:58)

问:可否动态组合 Future 呢?也就是说,当您需要向服务器进行某些 Pull 操作,但是在操作完成之前又无法知道需要多少 Pull 操作的时候,对于 API 的使用者来说,能否用简单的 Future 来完成这项操作呢?

Michael:这个时候您需要使用 Promise。您不应该去试图组合 onCompleteonSuccess, 我们已经有一个方法表明可以前往服务器,然后尝试拉取数据,接着如果读取失败的话就返回,而不应该去封装这个步骤,我们只要找到这个所需的 Promise 对象即可,非常简单,呃,我需要尝试多少次呢?完全无需进行重试,睡一觉起来再试就行了。所以实际上这个问题并不是很困难,但是正如您所说的,这并不是一 个很直观的思路。

问:我所开发的 App 已经用了很多 Reactive 了,比如 RxSwift。我在想您能否分享一些 Future/Promise 与 Reactive/Rx 之间互用的编程范式呢?

Michael: 典型的例子就是:您可能会需要进行一系列操作,然后您想要将其插入到某个异步操作当中。那么使用 Future 和 ReactiveCocoa 来构建动作是非常简单的。与此同时,也有很多 Reactive 能实现而 Future 不能实现的操作。我通常情况下会告诉人们,Future 的好处在于降低组合的难度。我们可以把多个 Future 组合在一起,然后同时等到一个期望的结果。这就是 Future 的优势所在。

参考资料

本文文字及图片出自 realm.io

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注