跳转至

第13章:资源管理

在之前的章节中,您发现,您有时不想重复您的努力,而是想共享网络请求、图像处理和文件解码等资源。任何可以避免多次重复的资源密集型内容都值得研究。换句话说,您应该在多个订阅者之间共享单个资源的结果——发布者作品产生的值——而不是重复该结果。

Combine提供了两个操作符来管理资源:share()操作符和multicast(_:)操作符。

share()操作符

此操作符的目的是让您通过引用而不是值获取发布者。发布者通常是结构:当您将发布者传递给函数或将其存储在多个属性中时,Swift会复制它几次。当您订阅每个副本时,发布者只能做一件事:开始它设计的工作并交付价值。

share()操作符返回Publishers.Share类的实例。通常,发布者作为struct实现,但在share()的情况下,如前所述,操作符获得对Share发布者的引用,而不是使用值语义,这允许它共享底层发布者。

这个新发布者“共享”上游发布者。它将订阅上游发布者一次,以及第一个传入的订阅者。然后,它将从上游发布者那里收到值转发给此订阅者和所有订阅者。

Note

新订阅者只会收到上游发布者在订阅后发出的值。不涉及缓冲或重播。如果订阅者在上游发布者完成后订阅共享发布者,则该新订阅者只会收到完成事件。

要将这个概念付诸实践,请想象您正在执行网络请求,就像您在第9章“网络”中学会了如何操作一样。您希望多个订阅者在不多次请求的情况下收到结果。您的代码如下所示:

let shared = URLSession.shared
  .dataTaskPublisher(for: URL(string: "https://www.raywenderlich.com")!)
  .map(\.data)
  .print("shared")
  .share()

print("subscribing first")

let subscription1 = shared.sink(
  receiveCompletion: { _ in },
  receiveValue: { print("subscription1 received: '\($0)'") }
)

print("subscribing second")

let subscription2 = shared.sink(
  receiveCompletion: { _ in },
  receiveValue: { print("subscription2 received: '\($0)'") }
)

第一个订阅者触发share()上游发布者的“工作”(在本例中,执行网络请求)。第二个订阅者只需“连接”它,并与第一个订阅者同时接收值。

Playground运行此代码,您会看到类似于以下内容的输出:

subscribing first
shared: receive subscription: (DataTaskPublisher)
shared: request unlimited
subscribing second
shared: receive value: (303425 bytes)
subscription1 received: '303425 bytes'
subscription2 received: '303425 bytes'
shared: receive finished

使用print(_:to:)操作符的输出,您可以看到:

  • 第一个订阅会触发DataTaskPublisher的订阅。
  • 第二个订阅不会改变任何事情:发布者继续运行。没有第二个请求发出。
  • 当请求完成时,发布者会向两个订阅者发送生成的数据,然后完成。

要验证请求是否只发送一次,您可以注释share()行,输出看起来类似于此:

subscribing first
shared: receive subscription: (DataTaskPublisher)
shared: request unlimited
subscribing second
shared: receive subscription: (DataTaskPublisher)
shared: request unlimited
shared: receive value: (303425 bytes)
subscription1 received: '303425 bytes'
shared: receive finished
shared: receive value: (303425 bytes)
subscription2 received: '303425 bytes'
shared: receive finished

您可以清楚地看到,当DataTaskPublisher不共享时,它会收到两个订阅!在这种情况下,请求运行两次,每次订阅一次。

但有一个问题:如果第二个订阅者是在共享请求完成后来的呢?您可以通过延迟第二个订阅来模拟此案例。

如果您在Playground上关注,请不要忘记取消评论share()。然后,将subscription2代码替换为以下内容:

var subscription2: AnyCancellable? = nil

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
  print("subscribing second")

  subscription2 = shared.sink(
    receiveCompletion: { print("subscription2 completion \($0)") },
    receiveValue: { print("subscription2 received: '\($0)'") }
  )
}

运行此操作,您会看到,如果延迟超过请求完成所需的时间,subscription2将不会收到任何东西:

subscribing first
shared: receive subscription: (DataTaskPublisher)
shared: request unlimited
subscribing second
shared: receive value: (303425 bytes)
subscription1 received: '303425 bytes'
shared: receive finished
subscribing second
subscription2 completion finished

到创建subscription2时,请求已经完成,生成的数据已经发出。如何确保两个订阅都收到请求结果?

multicast(_:)操作符

要将单个订阅共享给发布者,并在上游发布者完成后将值重播给新订阅者,您需要类似shareReplay()操作符的东西。不幸的是,这个操作符不是Combine的一部分。但是,您将在第18章“自定义发布者和处理背压”中学习如何创建一个。

在第9章“网络”中,您使用了multicast(_:)此操作符基于share(),并使用您选择Subject向订阅者发布值。multicast(_:)的独特特点是它返回的发布者是ConnectablePublisher。这意味着,在您调用其connect()方法之前,它不会订阅上游发布者。这使您有足够的时间设置所需的所有订阅者,然后将其连接到上游发布者并开始工作。

要调整上一个示例以使用multicast(_:),您可以编写:

// 1
let subject = PassthroughSubject<Data, URLError>()

// 2
let multicasted = URLSession.shared
  .dataTaskPublisher(for: URL(string: "https://www.raywenderlich.com")!)
  .map(\.data)
  .print("multicast")
  .multicast(subject: subject)

// 3
let subscription1 = multicasted
  .sink(
    receiveCompletion: { _ in },
    receiveValue: { print("subscription1 received: '\($0)'") }
  )

let subscription2 = multicasted
  .sink(
    receiveCompletion: { _ in },
    receiveValue: { print("subscription2 received: '\($0)'") }
  )

// 4
let cancellable = multicasted.connect()

以下是此代码的作用:

  1. 准备一个主题,中继上游发布者发出的值和完成事件。
  2. 使用上述subject准备多播发布者。
  3. 订阅共享(即多播)发布者,就像本章前面一样。
  4. 指示发布者连接到上游发布者。

这有效地开始了工作,但只有在您有时间设置所有订阅之后。这样,您可以确保没有订阅者会错过下载的数据。

如果您在Playground运行它,由此产生的输出将是:

multicast: receive subscription: (DataTaskPublisher)
multicast: request unlimited
multicast: receive value: (303425 bytes)
subscription1 received: '303425 bytes'
subscription2 received: '303425 bytes'
multicast: receive finished

Note

多播发布者与所有ConnectablePublisher一样,也提供autoconnect()方法,使其像share()一样工作:首次订阅它时,它会连接到上游发布者并立即开始工作。这在上游发布者发出单个值的场景中非常有用,您可以使用CurrentValueSubject与订阅者共享。

对于大多数现代应用程序来说,共享订阅工作,特别是对于网络等资源繁重的流程来说,是必不可少的。不关注这一点不仅会导致内存问题,还可能导致大量不必要的网络请求轰炸您的服务器。

Future

虽然share()multicast(_:)为您提供了成熟的发布者,但Combine还有一种方法,可以让您分享计算结果:Future,您在第2章“发布者和订阅者”中了解到了这一点。

您通过将收到Promise参数的闭包交给它来创建Future。每当您有成功或失败的结果时,您都会进一步履行承诺。看一个例子来刷新你的记忆:

// 1
func performSomeWork() throws -> Int {
  print("Performing some work and returning a result")
  return 5
}

// 2
let future = Future<Int, Error> { fulfill in
  do {
    let result = try performSomeWork()
    // 3
    fulfill(.success(result))
  } catch {
    // 4
    fulfill(.failure(error))
  }
}

print("Subscribing to future...")

// 5
let subscription1 = future
  .sink(
    receiveCompletion: { _ in print("subscription1 completed") },
    receiveValue: { print("subscription1 received: '\($0)'") }
  )

// 6
let subscription2 = future
  .sink(
    receiveCompletion: { _ in print("subscription2 completed") },
    receiveValue: { print("subscription2 received: '\($0)'") }
  )

此代码:

  1. 提供Future执行的模拟工作(可能是异步)的函数。
  2. 创造新的Future。请注意,工作立即开始,无需等待订阅者。
  3. 如果工作成功,它会以结果履行Promise
  4. 如果工作失败,它会将错误传递给Promise
  5. 订阅一次,以表明我们收到了结果。
  6. 第二次订阅,以表明我们也在不执行两次工作的情况下也收到了结果。

从资源的角度来看,有趣的是:

  • Future是一个类,而不是一个结构。
  • 创建后,它会立即调用您的关闭,以开始计算结果并尽快履行承诺。
  • 它存储已兑现Promise的结果,并将其交付给当前和未来的订阅者。

在实践中,这意味着Future是一种方便的方式,可以立即开始执行某些工作(无需等待订阅),同时只执行一次工作,并将结果交付给任何数量的订阅者。但它执行工作并返回单个结果,而不是一个结果流,因此用例比成熟的发布者更窄。

当您需要共享网络请求产生的单个结果时,它是一个很好的候选!

Note

即使您从未订阅过Future,创建它也会调用您的关闭并执行工作。您不能依赖Deferred在订阅者到来之前推迟关闭执行,因为Deferred是一个结构,每次有新订阅者都会创建新的Future

关键点

  • 在处理网络等资源繁重的流程时,共享订阅工作至关重要。
  • 当您只需要与多个订阅者共享发布者时,请使用share()
  • 当您需要精细控制上游发布者何时开始工作以及值如何向订阅者传播时,请使用multicast(_:))。
  • 使用Future将计算的单个结果共享给多个订阅者。

从这里去哪里?

恭喜您完成本节的最后一个理论迷你章节!

您将通过实践项目来总结本节,您将构建一个API客户端来与黑客新闻API进行交互。是时候继续前进了!