Combine之核心概念
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(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)
}
复制代码
我们仔细分析上边的代码,同样会得到以下几个重要信息:
-
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 : 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(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(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map
}
复制代码
看到了吗?.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发送request -
Publisher
调用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
映射成姓名。
为了让读者能够理解某些数据处理过程,我会引入一些必要的动画演示:
上图就采用了动画的形式演示了collect
Operator的数据收集功能。
还有Publisher数据合并的例子:
弹珠图是一个非常好的学习示例,基本上看弹珠图就能理解Operator的功能,在SwiftUI中实现这些UI,实在是太简单了。
总结
Combine是一个很强的函数响应式编程框架,不管是编写SwiftUI程序,还是UIKit程序,都需要考虑Combine,然后把处理数据的异步过程交给Combine来实现。