书回正文,所谓的响应式编程到底是什么呢?
熟悉 Combine 的同学可以直接跳到实践建议部分。
响应式编程
维基百科对响应式编程的定义是:
在计算中,响应式编程是一种面向数据流和变化传播的声明式编程范式。
虽然定义中每个字都认识,但连起来却十分费解。我们可以把定义中的内容分开来理解,逐个击破。首先,让我们来看下声明式编程。
声明式编程
声明式和指令式编程是常见的编程范式。在指令式编程中,开发者通过组合运算、循环、条件等语句让计算机执行程序。声明式与指令式正相反,如果说指令式像是告诉计算机 How to do,而声明式则是告诉计算机 What to do。其实大家都接触过声明式编程,但在编码时并不会意识到。各类 DSL 和函数式编程都属于声明式编程的范畴。
举个例子,假设我们想要获取一个整形数组里的所有奇数。按照指令式的逻辑,我们需要把过程拆解为一步一步的语句:
- 遍历数组中的所有元素。
- 判断是否为奇数。
- 如果是的话,加入到结果中。继续遍历。
var results = [Int]()for num in values { if num %2 != 0 { results.append(num) }}
如果按声明式编程来,我们的想法可能是“过滤出所有奇数”,对应的代码就十分直观:
var results = values.filter { $0 % 2 != 0 }
可见上述两种编程方式有着明显的区别:
“面向数据流和变化传播”
用说人话的方式解释,面向数据流和变化传播是响应未来发生的事件流。
- 事件发布:某个操作发布了事件 A,事件 A 可以携带一个可选的数据 B 。
- 操作变形:事件 A 与数据 B 经过一个或多个的操作发生了变化,最终得到事件 A’ 与数据 B’。
- 订阅使用:在消费端,有一个或多个订阅者来消费处理后的 A’ 和B’,并进一步驱动程序其他部分 (如 UI )
在这个流程中,无数的事件组成了事件流,订阅者不断接受到新的事件并作出响应。
至此,我们对响应式编程的定义有了初步的理解,即以声明的方式响应未来发生的事件流。在实际编码中,很多优秀的三方库对这套机制进一步抽象,为开发者提供了功能各异的接口。在 iOS 开发中,有三种主流的响应式“流派“。
响应式流派
这三个流派分别是 ReactiveX、Reactive Streams 和 Reactive*。ReactiveX 接下来会详细介绍。Reactive Stream 旨在定义一套非阻塞式异步事件流处理标准,Combine 选择了它作为实现的规范。以 ReactiveCocoa 为代表的 Reactive* 在 Objective-C 时代曾非常流行,但随着 Swift 崛起,更多开发者选择了 RxSwift 或 Combine,导致 Reactive* 整体热度下降不少。
ReactiveX (Reactive Extension)
ReactiveX 最初是微软在 .NET 上实现的一个响应式的拓展。它的接口命名并不直观,如 Observable (可观测的) 和 Observer(观测者)。ReactiveX 的优势在于创新地融入了许多函数式编程的概念,使得整个事件流的变形非常灵活。这个易用且强大的概念迅速被各个语言的开发者青睐,因此 ReactiveX 在很多语言都有对应版本的实现(如 RxJS,RxJava,RxSwift),都非常流行。Resso 的 Android 团队就在重度使用 RxJava。
为何选择 Combine
Combine 是 Apple 在 2019 年推出的一个类似 RxSwift 的异步事件处理框架。
通过对事件处理的操作进行组合 (combine) ,来对异步事件进行自定义处理 (这也正是 Combine 框架的名字的由来)。Combine 提供了一组声明式的 Swift API,来处理随时间变化的值。这些值可以代表用户界面的事件, 络的响应,计划好的事件,或者很多其他类型的异步数据。
Resso iOS 团队也曾短暂尝试过 RxSwift,但在仔细考察 Combine 后,发现 Combine 无论是在性能、调试便捷程度上都优于 RxSwift,此外还有内置框架和 SwiftUI 官配的特殊优势,受其多方面优势的吸引,我们全面切换到了 Combine。
Combine 的优势
相较于 RxSwift,Combine 有很多优势:
性能优势
Combine 的各项操作相较 RxSwift 有 30% 多的性能提升。
Reference: Combine vs. RxSwift Performance Benchmark Test Suite
Debug 优势
由于 Combine 是一方库,在 Xcode 中开启了 Show stack frames without debug symbols and between libraries 选项后,无效的堆栈可以大幅的减少,提升了 Debug 效率。
// 在 GlobalQueue 中接受并答应出数组中的值[1, 2, 3, 4].publisher .receive(on: DispatchQueue.global()) .sink { value in print(value) }
Combine 接口
上文提到,Combine 的接口是基于 Reactive Streams Spec 实现的,Reactive Streams 中已经定义好了 Publisher, Subscriber,Subscription 等概念,Apple 在其上有一些微调。
具体到接口层面,Combine API 与 RxSwift API 比较类似,更精简,熟悉 RxSwift 的开发者能无缝快速上手 Combine。Combine 中缺漏的接口可以通过其他已有接口组成替代,少部分操作符也有开源的第三方实现,对生产环境的使用不会产生影响。
OpenCombine
细心的读者可能有发现 Debug 优势 的图中出现了一个 OpenCombine。Combine 万般好,但有一个致命的缺点:它要求的最低系统版本是 iOS 13,许多要维护兼容多个系统版本的 App 并不能使用。好在开源 区给力,实现了一份仅要求 iOS 9.0 的 Combine 开源实现:OpenCombine。经内部测试,OpenCombine 的性能与 Combine 持平。OpenCombine 使用上与 Combine 差距很小,未来如果 App 的最低版本升级至 iOS 13 之后,从 OpenCombine 迁移到 Combine 的成本也很低,基本只有简单的文本替换工作。公司内 Resso、剪映、醒图、Lark 都有使用 OpenCombine。
Combine 基础概念
上文提到,Combine 的概念基于 Reactive Streams。响应式编程中的三个关键概念,事件发布/操作变形/订阅使用,分别对应到 Combine 中的 Publisher, Operator 与 Subscriber。
Publisher
Publisher<Output, Failure: Error>
Publisher 是事件产生的源头。事件是 Combine 中非常重要的概念,可以分成两类,一类携带了值(Value),另外一类标志了结束(Completion)。结束的可以是正常完成(Finished)或失败(Failure)。
Events:- Value:Output- Completion - Finished - Failure(Error)
通常情况下, 一个 Publisher 可以生成 N 个事件后结束。需要注意的是,一个 Publisher一旦发出了Completion(可以是正常完成或失败),整个订阅将结束,之后就不能发出任何事件了。
Apple 为官方基础库中的很多常用类提供了 Combine 拓展 Publisher,如 Timer, NotificationCenter, Array, URLSession, KVO 等。利用这些拓展我们可以快速组合出一个 Publisher,如:
// `cancellable` 是用于取消订阅的 token,下文会详细介绍cancellable = URLSession.shared // 生成一个 https://example.com 请求的 Publisher .dataTaskPublisher(for: URL(string: "https://example.com")!) // 将请求结果中的 Data 转换为字符串,并忽略掉空结果,下面会详细介绍 compactMap .compactMap { String(data: $0.data, encoding: .utf8) } // 在主线程接受后续的事件 (上面的 compactMap 发生在 URLSession 的线程中) .receive(on: RunLoop.main) // 对最终的结果(请求结果对应的字符串)进行消费 .sink { _ in // } receiveValue: { resultString in self.textView.text = resultString }
此外,还有一些特殊的 Publisher 也十分有用:
Subscriber
Subscriber<Input, Failure: Error>
Subsriber 作为事件的订阅端,它的定义与 Publisher 对应,Publisher 中的 Output对应Subscriber 的 Input。常用的 Subscriber 有 Sink 和 Assign。
Sink 直接对事件流进行订阅使用,可以对 Value 和 completion 分别进行处理。
// 从数组生成一个 Publishercancellable = [1, 2, 3, 4, 5].publisher .sink { completion in // 处理事件流结束 } receiveValue: { value in // 打印会每个值,会依次打印出 1, 2, 3, 4, 5 print(value) }
Assign 是一个特化版的 Sink ,支持通过 KeyPath 直接进行赋值。
let textLabel = UILabel()cancellable = [1, 2, 3].publisher // 将 数字 转换为 字符串,并忽略掉 nil ,下面会详细介绍这个 Operator .compactMap { String($0) } .assign(to: .text, on: textLabel)
需要留意的是,如果用 assign 对 self 进行赋值,可能会形成隐式的循环引用,这种情况需要改用 sink 与 weak self 手动进行赋值。
Cancellable & AnyCancellable
细心的读者可能发现了上面出现了一个 cancellable。每一个订阅都会生成一个 AnyCancellable 对象,用于控制订阅的生命周期。通过这个对象,我们可以取消订阅。当这个对象被释放时,订阅也会被取消。
// 取消订阅cancellable.cancel()
需要注意的是,每一个订阅我们都需要持有这个 cancellable,否则整个订阅会立即被取消并结束掉。
Subscription
Publisher 和 Subscriber 之间是通过 Subscription 建立连接。理解整个订阅过程对后续深入使用 Combine 非常有帮助。
图片来自《SwiftUI 和 Combine 编程》
Combine 的订阅过程其实是一个拉取模型。
- Subscriber 发起一个订阅,告诉 Publisher 我需要一个订阅。
- Publisher 返回一个订阅实体(Subscription)。
- Subscriber 通过这个 Subscription 去请求固定数量(Demand)的数据。
- Publisher 根据 Demand 返回事件。单次的 Demand 发布完成后,如果 Subscriber继续请求事件,Publisher 会继续发布。
- 继续发布流程。
- 当 Subscriber 请求的事件全部发布完成后,Publisher 会发送一个 Completion。
Subject
Subject<Output, Failure: Error>
Subject 是一类特殊的 Publisher,我们可以通过方法调用(如 send())手动向事件流中注入新的事件。
private let isPlayingPodcastSubject = CurrentValueSubject<Bool, Never>(false)// 向 isPlayingPodcastPublisher 注入一个新的事件,它的值是 trueisPlayingPodcastSubject.send(true)
Combine 提供了两个常用的 Subject:PassthroughSubject 与 CurrentValueSubject。
@Published
对于刚接触 Combine 的同学来说,最困扰的问题莫过于难以找到可以直接使用的事件源。Combine 提供了一个 Property Wrapper @Pubilshed 可以快速封装一个变量得到一个 Publisher。
// 声明变量class Alarm { @Published public var countDown = 0}let alarm = Alarm()// 订阅变化let cancellable = alarm.$countDown // Published<Int>.Publisher .sink { print($0) }// 修改 countDown,上面 sink 的闭包会触发alarm.countDown += 1
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!