Combine | (VI) 自定义 Publisher & 处理 Backpressure

8,394 阅读17分钟

在我们学习 Combine 的过程中,我们可能会觉得框架中缺少很多 Operator。反应式框架通常提供丰富的 Operator 生态系统,包括内置的和第三方的。Combine 允许我们创建自己的 Publisher,我们将了解如何实现。此外,我们将学习背压(Backpressure),我们将了解什么是背压以及如何创建处理它的 Publisher。

创建自定义 Publisher

创建自定义 Publisher 的方式有多种,难度从“简单”到“复杂”不等。对于我们实现的每个 Operator,我们将寻求最简单的实现形式,我们将了解创建自定义 Publisher 的三种不同方法:

  • 在 Publisher 命名空间中使用简单的扩展方法。

  • 在 Publishers 命名空间中使用产生值的 Subscription 实现一个类型。

  • 使用 Subscription 转换来自上游 Publisher 的值。

从技术上讲,可以在没有自定义 Subscription 的情况下创建自定义 Publisher。但如果这样做,将失去应对 Subscriber demand 的能力,这使该 Publisher 在 Combine 生态系统中是非法的。提前取消也会成为一个问题,这不是推荐的方法。

Publisher 作为扩展方法

假如,我们想创建一个新的 unwrap() Operator,它解开可选值并忽略 nil 值。我们可以重用现有的 compactMap(_:) Operator 来进行实现。我们将在 Publisher 命名空间中添加这个 Operator。

在 Plaoground 中添加以下代码:

extension Publisher {
    func unwrap<T>() -> Publishers.CompactMap<Self, T> where Output == Optional<T> {
        compactMap { $0 }
    }
}

将自定义 Operator 编写为方法,最复杂的部分是函数签名。Operator 实现很简单:只是在 self 上调用 compactMap(_:)

方法的签名的制作有些复杂。第一步是使 Operator 通用,因为它的输出是上游 Publisher 的包装类型,使用单个 compactMap(_:),因此返回类型为 -> Publishers.CompactMap<Self, T>。我们查看 Publishers.CompactMap,会发现它是一个泛型类型:public struct CompactMap<Upstream, Output>。在实现自定义 Operator 时,Upstream 是 Self(我们正在扩展的 Publisher),Output 是包装的类型。最后,我们将 Operator 限制为 Optional 类型 where Output == Optional<T> {

注意:当开发更复杂的 Operator 作为方法时,函数签名会变得非常复杂。一个好的技巧是让 Operator 返回一个 AnyPublisher<OutputType, FailureType>。返回一个以 eraseToAnyPublisher() 结尾的 Publisher,以对函数签名进行类型擦除。

现在我们可以测试我们的新 Operator 了,在下方添加代码:

let values: [Int?] = [1, 2, nil, 3, nil, 4]
values.publisher
    .unwrap()
    .sink {
        print("Received value: \($0)")
    }

运行 Playground,只有非 nil 值会打印到调试控制台:

Received value: 1
Received value: 2
Received value: 3
Received value: 4

Subscription 机制

Subscription 是 Combine 的重要部分:虽然我们到处都能看到 Publisher,但它们大多是无生命的实体。当我们订阅 Publisher 时,会实例化一个 Subscription,Subscription 负责接收 Subscriber 的 demand 并产生事件。以下是 Subscription 生命周期的详细信息:

subscription.png

  1. Subscriber 订阅 Publisher。
  2. Publisher 创建一个 Subscription,然后将其交给Subscriber(调用 receive(subscription:)方法)。
  3. Subscriber 通过向 Subscription 发送所需数量的值(调用 Subscription 的 request(_:))从 Subscription 中请求值。
  4. Subscription 开始工作并开始发出值。它将它们一一发送给 Subscriber(调用 Subscriber 的 receive(_:) 方法)。
  5. 收到值后,Subscriber 返回一个新的 Subscribers.Demand,它会添加到先前的总 Demand 中。
  6. Subscription 会一直发送值,直到发送的值数量达到请求的总数。

如果 Subscription 发送的值与 Subscriber 请求的值一样多,它应该在发送更多值之前等待新的 Demand 请求。我们可以绕过此机制并继续发送值,但这会破坏 Subscriber 和 Subscription 之间的协议,并可能导致未定义的行为。

最后,如果出现错误或订阅的值源完成,Subscription 会调用 Subscriber 的 receive(completion:) 方法。

使用 DispatchSource 自定义的 DispatchTimer

DispatchTimerConfiguration

我们之前了解了 Timer.publish(),我们可以基于 Dispatch 的 DispatchSourceTimer 开发自定义的 Timer。

我们将首先定义一个配置结构,这将使 Subscriber 及其 Subscription 之间共 Timer 配置变得容易。将此代码添加到 Playground:

struct DispatchTimerConfiguration {
    let queue: DispatchQueue?
    let interval: DispatchTimeInterval
    let leeway: DispatchTimeInterval
    let times: Subscribers.Demand
}

我们希望 Timer 能够在某个队列上触发,但也可以不关心在哪个队列上,因此,可以使 queue 成为可选的。interval 是 Timer 触发的时间间隔。leeway 是可以延迟传递 Timer 事件的最大时间间隔。times 是想要接收的 Timer 事件数。

DispatchTimer Publisher

我们现在可以开始创建 DispatchTimer Publisher。 Publisher 的代码很简单,因为所有工作都发生在 Subscription 内。在 DispatchTimerConfiguration 下方添加代码:

extension Publishers {
    struct DispatchTimer: Publisher {
        typealias Output = DispatchTime
        typealias Failure = Never
        let configuration: DispatchTimerConfiguration
        init(configuration: DispatchTimerConfiguration) {
            self.configuration = configuration
        }
        func receive<S: Subscriber>(subscriber: S)
        where Failure == S.Failure, Output == S.Input {
            let subscription = DispatchTimerSubscription(
                subscriber: subscriber,
                configuration: configuration
            )
            subscriber.receive(subscription: subscription)
        }
    }
}

我们的 Timer 将当前时间作为 DispatchTime 值发出。 当然,它永远不会失败,所以 Publisher 的 Failure 类型是 Never

此外,实现了 Publisher 协议所需的 receive(subscriber:) 方法,它需要一个编译时特化来匹配 Subscriber 类型 where Failure == S.Failure, Output == S.Input。大部分动作将发生在 DispatchTimerSubscription 中。Subscriber 会收到一个 Subscription,然后它可以向该 Subscription 发送值的请求。

DispatchTimerSubscription

Subscription 的作用包括:

  • 接受 Subscriber 的初始 Demend。

  • 按需生成 Timer 事件。

  • 每次 Subscriber 收到一个值并返回一个 Demend 时,都添加到 Demend 计数。

  • 确保它不会提供比配置中要求的更多的值。

开始在 Publishers 的扩展下方定义 Subscription:

private final class DispatchTimerSubscription <S: Subscriber>: Subscription where S.Input == DispatchTime {
}

此 Subscription 在外部不可见,只能通过 Subscription 协议,因此我们将其设为 private。这是一个类,我们可以通过引用传递它,Subscriber 可以将它添加到 Cancellable set 中,也可以保留它并独立调用 cancel()。当然,它用于 Input 值类型为 DispatchTime 的 Subscriber,这是 Subscription 发出的值的类型。

将这些属性添加到 Subscription 类的定义中:

private final class DispatchTimerSubscription <S: Subscriber>: Subscription where S.Input == DispatchTime {
    let configuration: DispatchTimerConfiguration
    var times: Subscribers.Demand
    var requested: Subscribers.Demand = .none
    var source: DispatchSourceTimer? = nil
    var subscriber: S?
}

包括:Subscriber 的 configuration。从 configuration 中获取的 Timer 将触发的最大次数 times,将使用它作为每次发送值时递减的计数器。requested 是当前的 Demend,每次发送值时都会减少它。source 是内部的 DispatchSourceTimer,将生成 Timer 事件。以及 subscriber ,这表明,只要 Subscription 没有完成、失败或取消,Subscription 就有责任保留 Subscriber。

DispatchTimerSubscription 定义中添加一个初始化方法:

init(subscriber: S,
     configuration: DispatchTimerConfiguration) {
    self.configuration = configuration
    self.subscriber = subscriber
    self.times = configuration.times
}

这很简单,将 times 设置为 Publisher 应接收 Timer 事件的最大次数,每次 Publisher 发出事件时,times 都会递减。当它达到零时,Timer 以完成事件结束。

接着实现 cancel(),Subscription 必须提供的必需方法:

func cancel() {
    source = nil
    subscriber = nil
}

DispatchSourceTimersource 设置为 nil 阻止它运行。将 Subscriber 属性设置为 nil 会其从Subscription 的范围中释放出来。

我们现在可以开始编写 Subscription 的核心代码:request(_:),一旦 Subscriber 通过 Publisher 获得 Subscription,Subscriber 必须从 Subscription 中请求值:

func request(_ demand: Subscribers.Demand) {
    guard times > .none else {
        subscriber?.receive(completion: .finished)
        return
    }
    requested += demand
    if source == nil, requested > .none {
    		// todo
    }
}

这个方法接收来自 Subscriber 的 Demand。Demand 是累积的:它们加起来形成 Subscriber 请求的值的总数。如果 Subscription 已经发送了最大数量的值,可以通知 Subscriber 已完成发送值。

接着通过添加新 Demand 来增加请求值的总数。检查 Timer 是否已经存在。如果没有,并且请求的值存在,那么是时候启动它了。

将此代码添加到最后一个的 if 内:

let source = DispatchSource.makeTimerSource(queue: configuration.queue)
source.schedule(deadline: .now() + configuration.interval,
                repeating: configuration.interval,
                leeway: configuration.leeway)
source.setEventHandler { [weak self] in
    guard let self = self,
          self.requested > .none else { return }
    self.requested -= .max(1)
    self.times -= .max(1)
    _ = self.subscriber?.receive(.now())
    if self.times == .none {
        self.subscriber?.receive(completion: .finished)
    }
}
self.source = source
source.activate()

从配置的队列中创建 DispatchSourceTimer。安排 Timer 在每 configuration.interval 秒后触发。一旦 Timer 启动,将永远不会停止它,直到 Subscriber 取消 Subscription。

然后我们为 Timer 设置 EventHandler。 这是 Timer 每次触发时调用的闭包。验证当前是否有请求的值后,减少两个计数器,然后向 Subscriber 发送一个值。如果要发送的值的总数达到指定的最大值,可以认为 Publisher 已完成并发出完成事件。最后,激活 source

最后添加此扩展,以定义一个 Operator,以便轻松链接此 Publisher:

extension Publishers {
    static func timer(queue: DispatchQueue? = nil,
                      interval: DispatchTimeInterval,
                      leeway: DispatchTimeInterval = .nanoseconds(0),
                      times: Subscribers.Demand = .unlimited)
    -> Publishers.DispatchTimer {
        return Publishers.DispatchTimer(
            configuration: .init(queue: queue,
                                 interval: interval,
                                 leeway: leeway,
                                 times: times)
        )
    }
}

添加此代码以测试我们的 Timer:

let publisher = Publishers.timer(interval: .seconds(1),
                                 times: .max(6))
let subscription = publisher.sink { time in
    print("Timer emits: \(time)")
}

运行 Playground:

Timer emits: DispatchTime(rawValue: 8517897314669)
Timer emits: DispatchTime(rawValue: 8517921315409)
Timer emits: DispatchTime(rawValue: 8517945315453)
Timer emits: DispatchTime(rawValue: 8517969296810)
Timer emits: DispatchTime(rawValue: 8517993313227)
Timer emits: DispatchTime(rawValue: 8518017296767)

尽管 Subscription 在 Combine API 中几乎看不到,但正如我们刚刚发现的那样,Subscription 完成了大部分工作。

实现 ShareReplay Operator

ShareReplaySubscription

要实现 shareReplay(),我们需要:

  1. 符合 Subscription 协议的类型。这是每个 Subscriber 将收到的 Subscription。为确保我们能够应对每个 Subscriber 的 demand 和 cancel,每个 Subscriber 都将收到单独的 Subscription。

  2. 符合 Publisher 协议的类型。我们将把它实现为一个类,因为所有 Subscriber 都希望共享同一个实例。

在 Playground 中添加此代码,创建 Subscription 类:

fileprivate final class ShareReplaySubscription<Output, Failure: Error>: Subscription {
  let capacity: Int
  var subscriber: AnySubscriber<Output,Failure>? = nil
  var demand: Subscribers.Demand = .none
  var buffer: [Output]
  var completion: Subscribers.Completion<Failure>? = nil
}

我们使用 class 而不是 struct 来实现 Subscription:Publisher 和 Subscriber 都需要访问和改变 Subscription。重播缓冲区的最大容量 capacity 是我们在初始化时设置的常数。在订阅期间保留对 Subscriber 的引用,使用类型擦除的 AnySubscriber 可以使我们免于与纠结其类型。跟踪 Publisher 从 Subscriber 那里收到的累积 demand,以便我们可以准确地交付请求数量的值。将挂起的值存储在缓冲区 buffer 中,直到它们被传递给 Subscriber 或被丢弃。completion 保留潜在的完成事件,以便在新 Subscriber 开始请求值时立即将其交付给他们。

注意,我们保留了完成事件:Publisher 不知道请求什么时候发生,所以它将完成事件交给 Subscription,以便在正确的时间被交付。

接着,将初始化方法添加到 Subscription 定义中:

init<S>(subscriber: S,
        replay: [Output],
        capacity: Int,
        completion: Subscribers.Completion<Failure>?)
where S: Subscriber,
      Failure == S.Failure,
      Output == S.Input {
    self.subscriber = AnySubscriber(subscriber)
    self.buffer = replay
    self.capacity = capacity
    self.completion = completion
}

从上游 Pulisher 接收多个值并将它们设置在此 Subscription 实例上。具体来说:存储类型擦除的 Subscriber;存储上游 Pulisher 的当前缓冲区 buffer 、最大容量 capacity 和完成事件 completion(如果已发出)。

我们需要一种将完成事件中继给 Subscriber 的方法:

private func complete(with completion: Subscribers.Completion<Failure>) {
    guard let subscriber = subscriber else { return }
    self.subscriber = nil
    self.completion = nil
    self.buffer.removeAll()
    subscriber.receive(completion: completion)
}

将 Subscriber 设置为 nil;通过将完成事件设置为 nil 来确保只发送一次完成,然后清空缓冲区;将完成事件中继给 Subscriber。

我们还需要一种可以向 Subscriber 发出值的方法:

private func emitAsNeeded() {
    guard let subscriber = subscriber else { return }
    while self.demand > .none && !buffer.isEmpty {
        self.demand -= .max(1)
        let nextDemand = subscriber.receive(buffer.removeFirst())
        if nextDemand != .none {
            self.demand += nextDemand
        }
    }
    if let completion = completion {
        complete(with: completion)
    }
}

仅当缓冲区中有值并且有未完成的 Demand 时才发出值。将未完成的 Demand 减一。向 Subscriber 发送第一个值,并收到新的 Demand。将新 Demand 添加到未完成的总 Demand 中,但前提是它不是 .none。 否则将崩溃,因为 Combine 不会将 Subscribers.Demand.none 视为零,并且添加或减去 .none 将触发异常。如果完成事件未发送,请立即发送。

实现 Subscription 最重要的要求:

func request(_ demand: Subscribers.Demand) {
    if demand != .none {
        self.demand += demand
    }
    emitAsNeeded()
}

取消订阅的代码更加容易, 添加此代码:

func cancel() {
  complete(with: .finished)
}

与 Subscriber 一样,我们需要实现接受值的方法和完成事件,添加此方法以接受值:

func receive(_ input: Output) {
    guard subscriber != nil else { return }
    buffer.append(input)
    if buffer.count > capacity {
        buffer.removeFirst()
    }
    emitAsNeeded()
}

确保有 Subscriber 后,此方法将将值添加到 buffer。确保缓冲的值不要超过请求的容量将结果交付给 Subscriber。

添加以下方法来接受完成事件:

func receive(completion: Subscribers.Completion<Failure>) {
    guard let subscriber = subscriber else { return }
    self.subscriber = nil
    self.buffer.removeAll()
    subscriber.receive(completion: completion)
}

此方法删除 Subscriber,清空缓冲区,并将完成事件发送到下游。

ShareReplay Publisher

Publisher 通常是 Publishers 命名空间中的值类型(struct)。但有时将 Publisher 实现为 class 如 Publishers.Multicast 或 Publishers.Share 是有意义的。对于这个 Publisher,我们需要一个类,类似于 share()。不过这是例外,大多数情况下我们会使用 struct。

ShareReplaySubscription 后添加此代码:

extension Publishers {
    final class ShareReplay<Upstream: Publisher>: Publisher {
        typealias Output = Upstream.Output
        typealias Failure = Upstream.Failure
    }
}

我们希望多个 Subscriber 能够共享此 Operator 的单个实例,因此我们使用 class。它也是通用的,上游 Publisher 的最终类型作为参数。这个新的 Publisher 不会改变上游 Publisher 的输出或失败类型——它只是使用上游的类型。

将 Publisher 需要的属性添加到 ShareReplay 的定义中:

private let lock = NSRecursiveLock()
private let upstream: Upstream
private let capacity: Int
private var replay = [Output]()
private var subscriptions = [ShareReplaySubscription<Output, Failure>]()
private var completion: Subscribers.Completion<Failure>? = nil

因为我们将同时提供多个 Subscriber,所以我们需要一个锁 lock 来保证对可变变量的独占访问。保留对上游 Publisher upstream 的引用,我们将在 Subscription 生命周期的各个阶段需要它。我们可以在初始化期间指定重放缓冲区的最大记录容量 capacity。当然,我们还需要存储记录值 replay。我们提供多个 Subscriber,因此我们需要将 subscriptions 留以通知他们事件。每个 Subscriber 都从一个专用的 ShareReplaySubscription 获取其值。 Operator 即使在完成后也可以重播值,因此我们需要记住上游 Publisher 是否完成。

首先,将必要的初始化程序添加到你的 ShareReplay 发布者:

init(upstream: Upstream, capacity: Int) {
  self.upstream = upstream
  self.capacity = capacity
}

这里只是存储上游 Publisher upstream 和容量 capacity

添加将来自上游传入值中继到 Subscriber 的方法:

private func relay(_ value: Output) {
    lock.lock()
    defer { lock.unlock() }
    guard completion == nil else { return }
    replay.append(value)
    if replay.count > capacity {
        replay.removeFirst()
    }
    subscriptions.forEach {
        $0.receive(value)
    }
}

由于多个 Subscriber 共享此 Publisher,因此我们必须使用锁保护对可变变量的访问。仅在上游尚未完成时才中继值。将值添加到缓冲区并仅保留所需容量值。这些是重播给新 Subscriber 的内容。将缓冲的值中继到每个连接的 Subscriber。

添加这个方法来处理完成事件:

private func complete(_ completion: Subscribers.Completion<Failure>) {
    lock.lock()
    defer { lock.unlock() }
    self.completion = completion
    subscriptions.forEach {
        $0.receive(completion: completion)
    }
}

为未来的 Subscriber 保存完成事件。将其转发给每个连接的 Subscriber。

我们现在已准备好开始编写每个 Publisher 必须实现的 receive 方法。此方法将接收 Subscriber。它的职责是创建一个新 Subscription,然后将其交给 Subscriber。

添加此代码以开始定义此方法:

func receive<S: Subscriber>(subscriber: S)
where Failure == S.Failure,
      Output == S.Input {
    lock.lock()
    defer { lock.unlock() }
    let subscription = ShareReplaySubscription(
        subscriber: subscriber,
        replay: replay,
        capacity: capacity,
        completion: completion)
    subscriptions.append(subscription)
    subscriber.receive(subscription: subscription)
    
    guard subscriptions.count == 1 else { return }
    let sink = AnySubscriber(
        receiveSubscription: { subscription in
            subscription.request(.unlimited)
        },
        receiveValue: { [weak self] (value: Output) -> Subscribers.Demand in
            self?.relay(value)
            return .none
        },
        receiveCompletion: { [weak self] in
            self?.complete($0)
        }
    )
    upstream.subscribe(sink)
}

新 Subscription 引用 Subscriber 并接收当前 replaycapacitycompletion。我们保留 Subscription 以将未来的事件传递给它。我们将 Subscription 发送给 Subscriber,Subscriber 可能(现在或以后)开始请求值。

只向上游 Publisher 订阅一次。使用方便的 AnySubscriber 类,它接受闭包,并在订阅时立即请求 .unlimited 值以让 Publisher 运行完成。将我们收到的值转发给下游 Subscriber。使用我们从上游获得的完成事件来完成我们的 Publisher。

注意:我们最初可以请求 .max(self.capacity) 并仅接收它,但 Combine 是 Demand 驱动的!如果我们请求的值没有 Publisher 能够产生的那么多值,那么我们可能永远不会收到完成事件!

最后我们将 AnySubscriber 订阅到上游 Publisher。

我们的 Publisher 已完成!我们还需要一个便利的 Operator,帮助将这个新 Publisher 与其他 Publishe r联系起来。

将其作为扩展添加到 Playground 末尾的 Publishers 命名空间:

extension Publisher {
    func shareReplay(capacity: Int = .max)
    -> Publishers.ShareReplay<Self> {
        return Publishers.ShareReplay(upstream: self,
                                      capacity: capacity)
    }
}

我们现在拥有一个功能齐全的 shareReplay(capacity:) Operator。

测试 shareReplay(capacity:)

在 Playground 中提那件以下代码:

let subject = PassthroughSubject<Int,Never>()
let publisher = subject.shareReplay(capacity: 2)
subject.send(0)

let subscription1 = publisher.sink(
    receiveCompletion: {
        print("subscription1 completed: \($0)")
    },
    receiveValue: {
        print("subscription1 received \($0)")
    }
)
subject.send(1)
subject.send(2)
subject.send(3)

subscription1 将收到:

subscription1 received 1
subscription1 received 2
subscription1 received 3

接下来,创建第二个 Subscription 并发送更多值和完成事件:

let subscription2 = publisher.sink(
    receiveCompletion: {
        print("subscription2 completed: \($0)")
    },
    receiveValue: {
        print("subscription2 received \($0)")
    }
)
subject.send(4)
subject.send(5)
subject.send(completion: .finished)

重新运行,subscription2 将收到回放,subscription1 subscription2 将收到新值:

subscription1 received 1
subscription1 received 2
subscription1 received 3
subscription2 received 2
subscription2 received 3
subscription1 received 4
subscription2 received 4
subscription1 received 5
subscription2 received 5
subscription1 completed: finished
subscription2 completed: finished

添加一个稍有延迟的 Subscription,以确保它在 Publisher 完成后发生:

var subscription3: Cancellable? = nil

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    print("Subscribing to shareReplay after upstream completed")
    subscription3 = publisher.sink(
        receiveCompletion: {
            print("subscription3 completed: \($0)")
        },
        receiveValue: {
            print("subscription3 received \($0)")
        }
    )
}
subscription1 received 1
subscription1 received 2
subscription1 received 3
subscription2 received 2
subscription2 received 3
subscription1 received 4
subscription2 received 4
subscription1 received 5
subscription2 received 5
subscription1 completed: finished
subscription2 completed: finished
Subscribing to shareReplay after upstream completed
subscription3 received 4
subscription3 received 5
subscription3 completed: finished

一切符合预期,shareReplay(capacity:) 工作做得很好!

处理 Backpressure

在流体动力学中,背压是与通过管道的流体流动相反的阻力。在 Combine 中,它是对来自 Publisher 的值流的阻力。

通常, Subscriber 需要处理 Publisher 发出的值:

  • 处理高频数据,例如来自传感器的输入;

  • 执行大文件传输;

  • 在数据更新时渲染复杂的 UI;

  • 等待用户输入;

  • 处理 Subscriber 无法以传入速度跟上的传入数据。

Combine 提供的 Publisher - Subscriber 机制是灵活的。这是一种拉式设计,而不是推式设计。这意味着 Subscriber 要求Publisher 发出值并指定他们想要接收的数量。这种请求机制是自适应的:每次订阅者收到一个新值时,需求都会更新。这允许 Subscriber 在他们不想接收更多数据时通过“关闭水龙头”来处理背压,并在他们准备好接收更多数据时“打开它”。

注意:请记住,我们只能以累加的方式调整 Demand。可以在 Subscriber 每次收到新值时增加 Demand,方法是返回新的 .max(N).unlimited。或者可以返回 .none,表示需求不应增加。Subscriber 随后“挂机”以接收至少达到新的最大 Demand 的值。例如,如果之前的最大 Demand 是接收三个值,而 Subscriber 只接收到一个,则在订阅者的 receive(_:) 中返回 .none 不会“关闭水龙头”。当 Publisher 准备好发送值时, Subscriber 仍将最少接收两个值。

当更多值可用时会发生什么完全取决于我们的设计,我们可以:

  • 通过管理 Demand 来控制流量,以防止 Publisher 发送超出处理能力的值;

  • 缓冲值,直到可以处理它们 - 存在耗尽可用内存的风险;

  • 删除无法立即处理的值;

  • 根据要求对以上的一些组合。

除了上述之外,处理背压还可以采取以下形式:

  • 具有处理拥塞的自定义 Subscription 的 Publisher。

  • 在 Publisher 链末端提供值的 Subscriber。

我们将专注于实现后者,创建一个 sink 函数的可暂停变体。

使用可暂停的 sink 来处理背压

在 Playground 创建一个协议,让我们从暂停中恢复:

protocol Pausable {
    var paused: Bool { get }
    func resume()
}

这里不需要 pause() 方法,因为我们将在收到每个值时确定是否暂停。

接下来开始定义可暂停的 Subscriber:

final class PausableSubscriber<Input, Failure: Error>:
    Subscriber, Pausable, Cancellable {
    let combineIdentifier = CombineIdentifier()
}

可暂停 Publisher 既可以暂停也可以取消。这是 pausableSink 函数将返回的对象。这也是为什么我们将它作为一个 class 而不是一个 struct 来实现的原因:我们不希望一个对象被复制,并且我们需要在其生命周期的某些点上具有可变性。Subscriber 必须为 Combine 提供唯一标识符以管理和优化其 Publisher 流。

现在添加这些属性:

let receiveValue: (Input) -> Bool
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
private var subscription: Subscription? = nil
var paused = false

receiveValue 闭包返回一个 Bool:true 表示它可能会收到更多的值,false 表示订阅应该暂停。完成闭包将在收到来自 Publisher 的完成事件时被调用。保留 Subscription,以便它可以在暂停后请求更多值。当我们不再需要它时,你需要将此属性设置为 nil 以避免循环。根据 Pausable 协议公开 paused 属性。

接下来,将以下代码添加到 PausableSubscriber 以实现初始化方法并符合 Cancelable 协议:

init(receiveValue: @escaping (Input) -> Bool,
     receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void) {
    self.receiveValue = receiveValue
    self.receiveCompletion = receiveCompletion
}

func cancel() {
    subscription?.cancel()
    subscription = nil
}

初始化方法接受两个闭包,Subscriber 将在收到来自 Publisher 的新值和完成时调用它们。闭包类似于使用 sink 函数的闭包,但有一个例外:receiveValue 闭包返回一个布尔值以指示接收器是否准备好接受更多值,或者是否需要暂停 Subscription。取消 Subscription 时,不要忘记之后将其设置为 nil 以避免循环。

现在添加此代码以满足 Subscriber 的要求:

func receive(subscription: Subscription) {
    self.subscription = subscription
    subscription.request(.max(1))
}

func receive(_ input: Input) -> Subscribers.Demand {
    paused = receiveValue(input) == false
    return paused ? .none : .max(1)
}

func receive(completion: Subscribers.Completion<Failure>) {
    receiveCompletion(completion)
    subscription = nil
}

收到 Publisher 创建的 Subscription 后,将其存储以备后用,以便我们能够从暂停中恢复。立即请求一个值。 Subscriber 可以暂停,我们无法预测何时需要暂停。这里的策略是一个一个地请求值。

当接收到新值时,调用 receiveValue 并相应更新暂停状态。如果 Subscriber 被暂停,返回 .none 表示你现在不想要更多的值——记住,你最初只请求了一个。

接收到完成事件后,将其转发给 receiveCompletion,然后将 Subscription 设置为 nil,因为你不再需要它。

最后,实现 Pausable 的其余部分:

func resume() {
    guard paused else { return }
    paused = false
    subscription?.request(.max(1))
}

如果 Publisher paused,则请求一个值以重新开始。我们先在可以在 Publishers 命名空间中公开新的 pausableSink

在 Playground 的末尾添加以下代码:

extension Publisher {
    func pausableSink(
        receiveCompletion: @escaping ((Subscribers.Completion<Failure>) -> Void),
        receiveValue: @escaping ((Output) -> Bool))
    -> Pausable & Cancellable {
        let pausable = PausableSubscriber(
            receiveValue: receiveValue,
            receiveCompletion: receiveCompletion)
        self.subscribe(pausable)
        return pausable
    }
}

pausableSinksink 非常接近。唯一的区别是 receiveValue 闭包的返回类型:Bool。实例化一个新的 PausableSubscriber 并将其 subscribe。Subscriber 是我们用来恢复和取消 Subscription 的对象。

测试 pausableSink

let subscription = [1, 2, 3, 4, 5, 6]
    .publisher
    .pausableSink(receiveCompletion: { completion in
        print("Pausable subscription completed: \(completion)")
    }) { value -> Bool in
        print("Receive value: \(value)")
        if value % 2 == 1 {
            print("Pausing")
            return false
        }
        return true
    }

Publisher 通常按顺序发出所有值。使用 pausableSink,此发布者将在收到值 1、3 和 5 时暂停。运行 Playground:

Receive value: 1
Pausing

要恢复 Publisher,需要异步调用 resume()。使用 Timer 很容易做到这一点:

let timer = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .sink { _ in
        guard subscription.paused else { return }
        print("Subscription is paused, resuming")
        subscription.resume()
    }

再次运行 Playground:

Receive value: 1
Pausing
Subscription is paused, resuming
Receive value: 2
Receive value: 3
Pausing
Subscription is paused, resuming
Receive value: 4
Receive value: 5
Pausing
Subscription is paused, resuming
Receive value: 6
Pausable subscription completed: finished

注意:如果 Publisher 无法保存值并等待 Subscriber 请求它们怎么办?在这种情况下,我们需要使用 buffer(size:prefetch:whenFull:) Operator 来缓冲值。此 Operator 可以将值缓冲到 size 参数中指定的容量,并在 Subscriber 准备好接收它们时传递它们,其他参数确定缓冲区如何填满以及缓冲区满时会发生什么。

内容参考