Combine解决的核心问题是如何处理时间序列数据,也就是随着时间变化而变化的数据。它有3大核心概念:Publisher,Operator和Subscriber:
Publisher是数据的提供者,它提供了最原始的数据,不管这个数据是从什么地方获取的。如果把pipline想象成一条包子生产线,那么Publisher就表示食材Subscriber是数据的接收者,它要求接收的数据必须是处理好的,同样把pipline想象成一条包子生产线,则Subscriber就是成品包子,而不是中间产物(菜馅等)Operator是中间处理过程,它上下联通Publisher和Subscriber,对Publisher输出地数据进行处理,然后返回成品数据给Subscriber
注意,我们上边所说的数据并不是静态的,而是动态的,我们通常假设不知数据何时到来?是否发生异常?我们只需提前写好处理这些数据和异常的逻辑,当数据到来时,Subscriber自动去响应处理好的数据。
Publisher
我们已经知道Publisher的核心思想是提供数据,接下来,我们从代码方面着手,来进一步了解Publisher。
public protocol Publisher {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure : Error
/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``, after which it can receive values.
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
从上边的代码,我们可以分析出3点重要信息:
Publisher是一个协议,我们后续用到的所有publishers都实现了这个协议Publisher/receive(subscriber:)是该协议的核心方法,它接受的参数subscriber需要实现Subscriber协议,这就是Operator和Subscriber能够连接Publisher原因Self.Failure == S.Failure, Self.Output == S.Input这个条件限制了Publisher输出的数据类型必须跟Subscriber输入的数据类型保持一致
Publisher/receive(subscriber:)并没有限制Subscriber的数量,因此Publisher可以接受多个订阅。
Subscriber
public protocol Subscriber : CustomCombineIdentifierConvertible {
/// The kind of values this subscriber receives.
associatedtype Input
/// The kind of errors this subscriber might receive.
///
/// Use `Never` if this `Subscriber` cannot receive errors.
associatedtype Failure : Error
/// Tells the subscriber that it has successfully subscribed to the publisher and may request items.
///
/// Use the received ``Subscription`` to request items from the publisher.
/// - Parameter subscription: A subscription that represents the connection between publisher and subscriber.
func receive(subscription: Subscription)
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Subscribers.Demand` instance indicating how many more elements the subscriber expects to receive.
func receive(_ input: Self.Input) -> Subscribers.Demand
/// Tells the subscriber that the publisher has completed publishing, either normally or with an error.
///
/// - Parameter completion: A ``Subscribers/Completion`` case indicating whether publishing completed normally or with an error.
func receive(completion: Subscribers.Completion<Self.Failure>)
}
我们仔细分析上边的代码,同样会得到以下几个重要信息:
Subscriber实现了CustomCombineIdentifierConvertible协议,用于标记唯一身份Subscriber同样是一个协议Subscriber/receive(subscription:)该方法由Subscriber实现,但是由Publisher来调用,Publisher调用了该方法后会传递一个实现了Subscription协议的实例,Subscriber使用该实例发送request请求数据Subscriber实现了Subscriber/receive(_:)协议,Publisher调用该方法发送数据Subscriber实现了Subscriber/receive(completion:)协议,Publisher调用该方法发送结束事件(.finished或.failure)Subscriber只接收输入数据
在真实的开发中,我们用到最多的Subscriber是assign和sink,后文会有他们的详细介绍。
Operator
我们在上文中讲到Operator连接了Publisher和Subscriber,这话即正确也不正确,说它不正确,是因为它还能连接Publisher和Publisher,或者说,它本身就是一个Publisher。
在Combine中,并没有Operator这么个协议,而我们所说的Operator指的是下边这些operators:
["scan", "tryScan", "map/tryMap", "flatMap","setFailureType",
"compactMap/tryCompactMap", "filter/tryFilter", "removeDuplicates",
"replace", "collect", "ignoreOutput", "reduce","max", "min", "count",
"first", "last", "drop", "prepend", "dropFirst", "prefix", "output",
"combineLatest","merge", "zip", "allSatisfy", "contains", "catch",
"assertNoFailure", "retry", "mapError", "switchToLatest", "debounce",
"delay", "measureInterval", "throttle", "timeout", "encode", "decode",
"share", "multicast","breakpoint", "breakpointOnError", "handleEvents",
"print", "receive", "subscribe"]
我们以最常用的map为例讲解一下代码层次的实现,其他的原理上都是一样的。
extension Publishers {
/// A publisher that transforms all elements from the upstream publisher with a provided closure.
public struct Map<Upstream, Output> : Publisher where Upstream : Publisher {
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The closure that transforms elements from the upstream publisher.
public let transform: (Upstream.Output) -> Output
public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output)
/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``, after which it can receive values.
public func receive<S>(subscriber: S) where Output == S.Input, S : Subscriber, Upstream.Failure == S.Failure
}
}
从上边的代码中,我们分析出以下几个重要信息:
- 结构体
Map实现了Publisher协议,因此它本身就是一个Publisher init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output)从它的初始化化函数来看,它需要传入一个upstream: Upstream,这个upstream就是一个Publisher,还需要传入一个闭包,闭包的参数是上游publisher的输出数据
总之一句话,Map接收一个Publisher作为输入,然后等待该publisher输出数据,然后把该数据映射成其他数据类型。
**注意,这个Map是一个结构体,并不是我们平时用的Operator。**我们平时是这么用的:
Just(1)
.map {
"\($0)"
}
.sink { print($0) }
这里的.map才是Operator,很明显,它是一个函数,我们看下它的定义:
extension Publisher {
public func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>
}
看到了吗?.map只是Publisher协议的一个扩展方法,它的返回值为Publishers.Map,也就是说,它返回了一个Map,这个Map结构体也是一个Publisher。
秒就秒在,.map是Publisher协议的一个方法,它又返回了一个实现了Publisher协议的实例,如此便实现了链式调用。
大家只要理解了上边的内容,就会对下边的代码大有所悟:
cancellable = publisher
.removeDuplicates()
.map { _ in
return "aaa"
}
.flatMap { value in
return URLSession.shared.dataTaskPublisher(for: URL(string: "https://xxx.com?name=\(value)")!)
}
.tryMap { (data, response) -> Data in
guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 else {
throw NetworkError.invalidResponse
}
return data
}
.decode(type: Student.self, decoder: JSONDecoder())
.catch { _ in
Just(Student(name: "", age: 0))
}
.sink(receiveCompletion: {
print($0)
}, receiveValue: {
print($0)
})
Subscription
Subscription也是一个不容忽视的概念,是它连接了Publisher和Subscriber,我们看看它的代码:
public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible {
/// Tells a publisher that it may send more values to the subscriber.
func request(_ demand: Subscribers.Demand)
}
同样,基于上边的代码,我们分析出以下几个信息:
- 它是一个协议,实现该协议的实例必须实现协议要求的方法
- 它继承了
Cancellable协议,因此实现了Subscription协议的实例自然就可以取消pipline - 使用
request函数发送请求
我们在下一小节,再讲解pipline具体的事件过程,上边的代码中还有一个需要理解的概念:Subscribers.Demand,它就是我们多次提到的请求是否受限制。
public enum Subscribers {
}
extension Subscribers {
/// A requested number of items, sent to a publisher from a subscriber through the subscription.
@frozen public struct Demand : Equatable, Comparable, Hashable, Codable, CustomStringConvertible {
/// A request for as many values as the publisher can produce.
public static let unlimited: Subscribers.Demand
/// A request for no elements from the publisher.
///
/// This is equivalent to `Demand.max(0)`.
public static let none: Subscribers.Demand
/// Creates a demand for the given maximum number of elements.
///
/// The publisher is free to send fewer than the requested maximum number of elements.
///
/// - Parameter value: The maximum number of elements. Providing a negative value for this parameter results in a fatal error.
@inlinable public static func max(_ value: Int) -> Subscribers.Demand
}
}
我们继续分析:
- 可以看出
Subscribers是一个enum,前边提到的.finished和.failure就是来自这个enum Demand是一个结构体,它的实例用于描述订阅请求是否受限,这是一个核心概念func max(_ value: Int) -> Subscribers.Demand该方法可以设置一个最大请求数,如果为0表示完全受限制,`Subscriber不能接收数据,如果设置一个具体的值,则最多可以接受这个值个数的数据
通常情况下,请求都是不受限的。
事件过程
从代码层次理解了Publisher,Subscriber,Operator和Subscription后,再回过头来看下边这个图,就不难理解了。
Publisher收到订阅Publisher调用Subscriber的Subscriber/receive(subscription:)方法返回一个subscription实例Subscriber使用subscription发送requestPublisher调用Subscriber的Subscriber/receive(_:)方法发送数据Publisher调用Subscriber的Subscriber/receive(completion:)方法发送完成事件
总结一下,当Publisher收到订阅后就拥有了这个订阅者,然后等待订阅着发出请求,再调用订阅者的方法传输数据和事件。
上边描述的内容算是经典模式,以宏观的角度来看问题,比较适合下边的代码:
Just(1)
.sink(receiveValue: { print($0) })
但是如果增加一些Operator,事情就变得有一点不一样了,我们看下边的例子:
Just(1)
.map {
"数字:\($0)"
}
.sink(receiveValue: { print($0) })
我们知道sink就是订阅者,它发送了一个request,这个request是如何传播到Just的呢?这就需要引入一个新的概念:back-pressure。所谓back-pressure指的就是数据请求是由订阅者发起的。
为什么要这么设计呢? 大家想一下,订阅者往往接受数据是为了刷新UI的,如果Publisher发送了大量数据,势必会造成UI刷新的性能问题。
接下来我们简单分析一下这个back-pressure的过程,我们不会把这个过程讲的很详细,大家只需要理解其中的核心思想就行了,先看下边这张图:
- 我们已经知道
.map实际上返回了一个Map示例,它实现了Publisher协议 - 上图中的
Just和Map,当收到订阅后,都会调用receive方法,然后返回一个实现了Subscription协议的实例 Map中的Subscription中存在一个实现了Subscriber协议的Sink实例,这个很关键- 当
sink订阅了Map后,Map的receive方法被调用,在该方法中,先使用Sink订阅其上游的Publisher(Just),然后返回Subscription - 也就是说当
sink订阅了Map后,他们就逆向的建立了连接,当sink发送请求后,代码就沿着上图中的绿色箭头方向调用,值得注意的是,由于Map中的Subscription中的Sink保存了Just的Subscription,因此需要Sink去调用Just中Subscription中的.request()方法
如果大家不理解我上边讲的这个过程也没关系,后续的文章中,我会讲到如何自定义Publisher,Operator和Subscriber,当然那些内容算是进阶内容,即使不理解这些知识也是没关系的。
Marble diagrams(弹珠图)
上图是最常见的弹珠图的示意图,在响应式编程的世界中,通常用弹珠图来演示Operator的功能,也就是让我们能够非常清晰地理解Operator是如何映射数据的。
本教程后续Operator的示例也使用弹珠图,但样式上会有一些不同,本教程中用到的所有弹珠图都是用SwiftUI编码实现的:
我把Operator的代码放在了弹珠图的中间位置,方便读者对照着数据和程序学习。弹珠图上的弹珠不一定只是数字,它可以是任何数据类型,比如结构图,对象等等:
上图中的弹珠使用了矩形表示,这么做的目的只是为了容纳更多的可展示的元素,上图中的输入数据是一个Student对象,通过map映射成姓名。
为了让读者能够理解某些数据处理过程,我会引入一些必要的动画演示:

上图就采用了动画的形式演示了collectOperator的数据收集功能。
还有Publisher数据合并的例子:
弹珠图是一个非常好的学习示例,基本上看弹珠图就能理解Operator的功能,在SwiftUI中实现这些UI,实在是太简单了。
总结
Combine是一个很强的函数响应式编程框架,不管是编写SwiftUI程序,还是UIKit程序,都需要考虑Combine,然后把处理数据的异步过程交给Combine来实现。