Combine之核心概念

3,052 阅读10分钟

image.png

Combine解决的核心问题是如何处理时间序列数据,也就是随着时间变化而变化的数据。它有3大核心概念:PublisherOperatorSubscriber

  • Publisher是数据的提供者,它提供了最原始的数据,不管这个数据是从什么地方获取的。如果把pipline想象成一条包子生产线,那么Publisher就表示食材
  • Subscriber是数据的接收者,它要求接收的数据必须是处理好的,同样把pipline想象成一条包子生产线,则Subscriber就是成品包子,而不是中间产物(菜馅等)
  • Operator是中间处理过程,它上下联通Publisher和Subscriber,对Publisher输出地数据进行处理,然后返回成品数据给Subscriber

注意,我们上边所说的数据并不是静态的,而是动态的,我们通常假设不知数据何时到来?是否发生异常?我们只需提前写好处理这些数据和异常的逻辑,当数据到来时,Subscriber自动去响应处理好的数据。

image.png

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协议,这就是OperatorSubscriber能够连接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只接收输入数据

在真实的开发中,我们用到最多的Subscriberassignsink,后文会有他们的详细介绍。

Operator

我们在上文中讲到Operator连接了PublisherSubscriber,这话即正确也不正确,说它不正确,是因为它还能连接PublisherPublisher,或者说,它本身就是一个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

秒就秒在,.mapPublisher协议的一个方法,它又返回了一个实现了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也是一个不容忽视的概念,是它连接了PublisherSubscriber,我们看看它的代码:

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,OperatorSubscription后,再回过头来看下边这个图,就不难理解了。

image.png

  1. Publisher收到订阅
  2. Publisher调用SubscriberSubscriber/receive(subscription:)方法返回一个subscription实例
  3. Subscriber使用subscription发送request
  4. Publisher调用SubscriberSubscriber/receive(_:)方法发送数据
  5. Publisher调用SubscriberSubscriber/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的过程,我们不会把这个过程讲的很详细,大家只需要理解其中的核心思想就行了,先看下边这张图:

back-pressure.001.png

  • 我们已经知道.map实际上返回了一个Map示例,它实现了Publisher协议
  • 上图中的JustMap,当收到订阅后,都会调用receive方法,然后返回一个实现了Subscription协议的实例
  • Map中的Subscription中存在一个实现了Subscriber协议的Sink实例,这个很关键
  • sink订阅了Map后,Mapreceive方法被调用,在该方法中,先使用Sink订阅其上游的Publisher(Just),然后返回Subscription
  • 也就是说当sink订阅了Map后,他们就逆向的建立了连接,当sink发送请求后,代码就沿着上图中的绿色箭头方向调用,值得注意的是,由于Map中的Subscription中的Sink保存了JustSubscription,因此需要Sink去调用JustSubscription中的.request()方法

如果大家不理解我上边讲的这个过程也没关系,后续的文章中,我会讲到如何自定义Publisher,Operator和Subscriber,当然那些内容算是进阶内容,即使不理解这些知识也是没关系的。

Marble diagrams(弹珠图)

image.png

上图是最常见的弹珠图的示意图,在响应式编程的世界中,通常用弹珠图来演示Operator的功能,也就是让我们能够非常清晰地理解Operator是如何映射数据的。

本教程后续Operator的示例也使用弹珠图,但样式上会有一些不同,本教程中用到的所有弹珠图都是用SwiftUI编码实现的:

企业微信截图_ece221de-6534-4045-adf0-571392c3fe0b.png

我把Operator的代码放在了弹珠图的中间位置,方便读者对照着数据和程序学习。弹珠图上的弹珠不一定只是数字,它可以是任何数据类型,比如结构图,对象等等:

企业微信截图_c4e9d095-ea01-4451-8b6c-f770fd22ae82.png

上图中的弹珠使用了矩形表示,这么做的目的只是为了容纳更多的可展示的元素,上图中的输入数据是一个Student对象,通过map映射成姓名。

为了让读者能够理解某些数据处理过程,我会引入一些必要的动画演示:

Kapture 2020-10-09 at 10.50.10.gif

上图就采用了动画的形式演示了collectOperator的数据收集功能。

还有Publisher数据合并的例子:

image.png

弹珠图是一个非常好的学习示例,基本上看弹珠图就能理解Operator的功能,在SwiftUI中实现这些UI,实在是太简单了。

总结

Combine是一个很强的函数响应式编程框架,不管是编写SwiftUI程序,还是UIKit程序,都需要考虑Combine,然后把处理数据的异步过程交给Combine来实现。