在我们学习 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 生命周期的详细信息:
- Subscriber 订阅 Publisher。
- Publisher 创建一个 Subscription,然后将其交给Subscriber(调用
receive(subscription:)方法)。 - Subscriber 通过向 Subscription 发送所需数量的值(调用 Subscription 的
request(_:))从 Subscription 中请求值。 - Subscription 开始工作并开始发出值。它将它们一一发送给 Subscriber(调用 Subscriber 的
receive(_:)方法)。 - 收到值后,Subscriber 返回一个新的
Subscribers.Demand,它会添加到先前的总 Demand 中。 - 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
}
将 DispatchSourceTimer 即 source 设置为 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(),我们需要:
-
符合 Subscription 协议的类型。这是每个 Subscriber 将收到的 Subscription。为确保我们能够应对每个 Subscriber 的 demand 和 cancel,每个 Subscriber 都将收到单独的 Subscription。
-
符合 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 并接收当前 replay、capacity 和 completion。我们保留 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
}
}
pausableSink 与 sink 非常接近。唯一的区别是 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 准备好接收它们时传递它们,其他参数确定缓冲区如何填满以及缓冲区满时会发生什么。
内容参考
- Combine | Apple Developer Documentation;
- 来自 Kodeco 的书籍《Combine: Asynchronous Programming with Swift》;
- 对上述 Kodeco 书籍的汉语自译版 《Combine: Asynchronous Programming with Swift》整理与补充。