第1章:你好,Combine
!¶
本书旨在向你介绍Combine
框架以及用Swift
为苹果平台编写声明式、反应式应用程序。
用苹果自己的话说:"Combine
框架为你的应用程序如何处理事件提供了一种声明式的方法。你可以为一个给定的事件源创建一个单一的处理链,而不是潜在地实现多个委托回调或完成处理闭包。该链的每一部分都是一个组合运算符,对从上一步收到的元素执行不同的操作。"
尽管非常准确,而且切中要害,这个令人愉快的定义一开始听起来可能有点太抽象了。这就是为什么,在深入进行编码练习和在下面的章节中进行项目工作之前,你要花一点时间了解一下Combine
解决的问题和它用来解决的工具。
一旦你积累了相关的词汇并对框架有了一定的了解,你就会在编码时开始涉及基础知识。
逐渐地,随着你在书中的进展,你会了解到更多的高级主题,并最终完成几个项目。
当你涵盖了其他所有内容后,你将在最后一章中研究一个用Combine
构建的完整应用程序。
异步编程¶
在一个简单的单线程语言中,程序是按顺序逐行执行的。例如,在伪代码中:
begin
var name = "Tom"
print(name)
name += " Harding"
print(name)
end
同步代码很容易理解,而且特别容易争论你的数据的状态。有了单线程的执行,你总是可以确定你的数据的当前状态是什么。在上面的例子中,你知道第一个print
将总是打印Tom
,第二个将总是打印"Tom Harding"
。
现在,想象一下,你用多线程语言编写的程序,正在运行一个异步事件驱动的UI
框架,比如在Swift
和UIKit
上运行的iOS
应用。
考虑一下可能发生的情况:
--- Thread 1 ---
begin
var name = "Tom"
print(name)
--- Thread 2 ---
name = "Billy Bob"
--- Thread 1 ---
name += " Harding"
print(name)
end
这里,代码将name
的值设置为"Tom"
,然后再加上"Harding"
,就像之前一样。但由于另一个线程可能同时执行,你的程序的其他部分有可能在name
的两次突变之间运行,并将其设置为另一个值,如"Billy Bob"
。
当代码在不同的核心上并发运行时,很难说哪一部分的代码会先修改共享状态。
上面的例子中,运行在"线程2"上的代码可能是。
- 与你的原始代码在不同的
CPU
核上完全同时执行。 - 在
name += " Harding"
之前执行,所以不是原始值"Tom"
,而是"Billy Bob"
。
当你运行这段代码时,到底会发生什么,取决于系统的负载,你每次运行程序时可能会看到不同的结果。
一旦你运行异步并发代码,管理你的应用程序中的易变状态就成了一项加载任务。
Foundation
和UIKit/AppKit
¶
多年来,苹果一直在不断改进其平台的异步编程。他们已经创建了几种机制,你可以在不同的系统层面上使用,以创建和执行异步代码。
你可以使用像用NSThread
管理你自己的线程那样的低级别的API
,一直到用async/await
结构使用Swift
的现代并发性。
你可能已经在你的应用程序中使用了以下一些东西:
NotificationCenter
:在感兴趣的事件发生时执行一段代码,例如当用户改变设备的方向或软件键盘在屏幕上显示或隐藏时。委托模式
:让你定义一个代表另一个对象行事或与之协调的对象。例如,在你的应用程序委托中,你定义了当一个新的远程通知到达时应该发生什么,但你不知道这段代码何时运行或执行多少次。Grand Central Dispatch and Operations
:帮助你抽象出工作片段的执行。你可以用它们来安排代码在一个串行队列中按顺序运行,或者在不同的队列中以不同的优先级并发运行大量的任务。闭包
:创建独立的代码片段,你可以在你的代码中传递,所以其他对象可以决定是否执行它,执行多少次,以及在什么情况下执行。
由于大多数典型的代码都是异步执行一些工作,而且所有的UI
事件都是固有的异步,所以不可能对整个应用程序代码的执行顺序做出假设。
然而,写好异步程序是可能的。它只是比......好吧,我们希望它更复杂。
当然,造成这些问题的原因之一是,一个坚实的、现实生活中的应用程序很可能使用所有不同种类的异步API
,每个都有自己的接口,像这样:
Combine
为Swift
生态系统引入了一种通用的高级语言来设计和编写异步代码。
苹果公司也将Combine
集成到其他框架中,所以Timer
、NotificationCenter
和Core Data
等核心框架已经使用它的语言。幸运的是,Combine
也很容易集成到你自己的代码中。
最后,但绝对不是最不重要的,苹果公司设计了他们惊人的UI
框架,SwiftUI
,也很容易与Combine
整合。
为了让你了解苹果公司对使用Combine
的反应式编程的承诺,这里有一个简单的图表,显示了Combine
在系统层次结构中的位置:
各种系统框架,从Foundation
一直到SwiftUI
,都依赖于Combine
,并提供Combine
集成作为其更"传统"的API
的替代品。
Swift
的现代并发性¶
Swift 5.5
引入了一系列用于开发异步和并发代码的API
,由于采用了新的线程池模型,你的代码可以安全、快速地随意暂停和恢复异步工作。
现代的并发性API
使许多经典的异步问题相当容易解决--例如,等待网络响应,并行运行多个任务,等等。
这些API
解决了一些与Combine
相同的问题,但Combine
的优势在于其丰富的操作符集。Combine
提供的处理事件的操作符使很多复杂的、常见的场景变得容易解决。
反应式操作符直接解决了网络、数据处理和UI事件处理中的各种常见问题,因此对于更复杂的应用,使用Combine
开发有很多好处。
而且,说到Combine
的优势,让我们快速看看到目前为止反应式编程的出色表现。
Combine
的基础¶
声明式、反应式编程并不是一个新概念。它已经存在了相当长的一段时间,但在过去的十年里,它有了相当明显的回潮。
第一个"现代 "的反应式解决方案出现在2009年,当时微软的一个团队推出了一个名为Reactive Extensions for .NET(Rx.NET)
的库。
微软在2012
年将Rx.NET
的实现开源,从那时起,许多不同的语言都开始使用它的概念。目前,有许多Rx
标准的端口,如RxJS
、RxKotlin
、RxScala
、RxPHP
等等。
对于苹果的平台,已经有几个第三方的反应式框架,如RxSwift
,它实现了Rx
标准;ReactiveSwift
,直接受到Rx
的启发;Interstellar
,它是一个自定义的实现,还有其他的。
Combine
实现了一个与Rx
不同但类似的标准,叫做Reactive Streams
。Reactive Streams
与Rx
有一些关键的区别,但它们都同意大多数的核心概念。
如果你以前没有使用过上述的一个或另一个框架--不要担心。到目前为止,对于苹果的平台来说,反应式编程一直是一个相当小众的概念,尤其是Swift
。
然而,在iOS 13/MacOS Catalina
中,苹果通过内置系统框架Combine
为其生态系统带来了反应式编程支持。
既然如此,就从学习Combine
的一些基础知识开始,看看它如何帮助你写出安全、可靠的异步代码。
Combine
基础知识¶
概括地说,Combine
中的三个关键活动部件是发布者、操作符和订阅者。当然,团队中还有更多的参与者,但如果没有这三个人,你就不能取得什么成就。
你将在第2章"发布者和订阅者"中详细了解发布者和订阅者,本书的第二部分将指导你尽可能多地熟悉操作符。
然而,在这一介绍性章节中,你将得到一个简单的速成课程,让你大致了解这些类型在代码中的用途,以及它们的职责是什么。
发布者¶
发布者是可以随着时间的推移向一个或多个相关方(如订阅者)发射值的类型。不管发布者的内部逻辑是什么,它几乎可以是任何东西,包括数学计算、网络或处理用户事件,每个发布者都可以发出这三种类型的多个事件。
- 一个发布者通用的
Output
类型的输出值。 - 一个成功的完成。
- 一个带有发布者的
Failure
类型的错误的完成。
一个发布者可以发出零个或更多的输出值,如果它曾经完成,无论是成功还是由于失败,它将不会发出任何其他事件。
下面是一个发射Int
值的发布者在时间轴上的可视化情况:
蓝色方框代表在时间轴上某一特定时间发出的值,数字代表发出的值。一条垂直线,就像你在图的右边看到的那条,代表一个成功的流完成。
三个可能事件的简单契约是如此普遍,以至于它可以代表你程序中的任何种类的动态数据。这就是为什么你可以使用Combine publishers
解决你的应用程序中的任何任务--不管它是关于计算数字、进行网络调用、对用户手势作出反应还是在屏幕上显示数据。
你不必总是在你的工具箱中为手头的任务寻找合适的工具,无论是添加一个委托还是注入一个完成回调,你都可以用发布者来代替。
发布者的一个最好的特点是它们内置了错误处理功能;错误处理并不是你在最后选择性地添加的,如果你想的话。
Publisher
协议在两种类型上是通用的,你可能已经在前面的图中注意到了。
Publisher.Output
是发布者的输出值的类型。如果它是一个Int
发布者,它永远不能发布String
或Date
值。Publisher.Failure
是发布者在失败时可以抛出的错误类型。如果发布者永远不能失败,你可以通过使用Never
失败类型来指定。
当你订阅一个给定的发布者时,你知道从它那里期待什么值以及它可能失败的错误。
操作符¶
操作符是在Publisher
协议上声明的方法,它返回相同或新的发布者。这非常有用,因为你可以一个接一个地调用一堆操作符,有效地将它们连锁起来。
因为这些被称为"操作符"的方法是高度解耦和可组合的,它们可以被组合起来(啊哈!),在单个订阅的执行过程中实现非常复杂的逻辑。
令人着迷的是,操作符像拼图一样紧密地结合在一起。它们不能被错误地放在一起,也不能在一个的输出与下一个的输入类型不匹配的情况下合在一起。
以一种明确的确定性方式,你可以定义每一个异步抽象工作的顺序,以及正确的输入/输出类型和内置错误处理。这几乎好得不能再好了!
作为额外的奖励,操作符总是有输入和输出,通常被称为上游和下游--这使他们能够避免共享状态(我们之前讨论的核心问题之一)。
操作符专注于处理他们从上一个操作符那里收到的数据,并将他们的输出提供给链中的下一个操作符。这意味着没有其他异步运行的代码可以"跳入"并改变你正在处理的数据。
订阅者¶
最后,你到达了订阅链的末端。每个订阅都以一个订阅者结束。订阅者通常对发出的输出或完成事件做一些"事情"。
目前,Combine
提供了两个内置的订阅者,这使得与数据流的工作变得简单明了。
sink
订阅者允许您为代码提供闭包,以接收输出值和完成。 从那里,你可以用接收到的事件做任何你内心渴望的事情。assign
订阅服务器允许您将生成的输出绑定到数据模型或UI
控件上的某个属性,以便通过键路径直接在屏幕上显示数据,而无需自定义代码。
如果你对你的数据有其他需求,创建自定义订阅者甚至比创建发布者更容易。Combine
使用了一套非常简单的协议,使你能够在车间没有提供合适的工具来完成你的任务时,建立自己的自定义工具。
订阅¶
Note
本书使用订阅一词来描述Combine
的Subscription
协议及其符合的对象,以及发布者、操作符和订阅者的完整链条。
当你在订阅的末尾添加一个订阅者时,它就会"激活"链上的所有发布者。这是一个奇怪但重要的细节,要记住--如果没有订阅者有可能接收输出,那么发布者就不会发出任何值。
订阅是一个奇妙的概念,因为它允许你声明一个异步事件链,有自己的自定义代码和错误处理,只需一次,然后你就不必再考虑它了。
如果你完全使用Combine
,你可以通过订阅来描述你整个应用程序的逻辑,一旦完成,只需让系统运行一切,而不需要推拉数据或回调这个或那个其他对象。
一旦订阅代码编译成功,并且你的自定义代码中没有逻辑问题--你就完成了 按照设计,每当发生一些事件,如用户的手势、定时器响起或其他东西唤醒你的一个发布者时,订阅就会异步地"发射"。
更好的是,你不需要专门的内存管理订阅,这要归功于Combine
提供的一个叫做Cancellable
的协议。
两个系统提供的订阅者都符合Cancellable
,这意味着你的订阅代码(例如整个发布者、操作符和订阅者的调用链)返回一个Cancellable
对象。每当你从内存中释放该对象时,它就会取消整个订阅,并从内存中释放其资源。
这意味着你可以很容易地"绑定"一个订阅的寿命,例如,将其存储在视图控制器的一个属性中。这样,当用户从视图堆栈中删除视图控制器时,就会取消其属性,也会取消你的订阅。
或者为了使这个过程自动化,你可以在你的类型上有一个[AnyCancellable]
集合属性,并在其中抛出你想要的许多订阅。当该属性从内存中释放时,它们都会被自动取消和释放。
正如你所看到的,有很多东西需要学习,但在详细解释时都是合乎逻辑的。而这正是接下来几章的计划--在本书结束时,把你从零开始慢慢地但稳定地带到Combine
英雄。
Combine
代码比"标准"代码有什么好处?¶
无论如何,你可以不使用Combine
,但仍然可以创建最好的应用程序。这一点无可非议。你也可以不用Core Data
、URLSession
,甚至UIKit
来创建最好的应用程序。但使用这些框架比自己构建这些抽象更方便、更安全、更高效。
Combine
(和其他系统框架)的目的是为你的异步代码添加另一个抽象。在系统层面上的另一个抽象层次意味着更紧密的集成,它经过良好的测试,是一种安全的技术。
Combine
是否适合你的项目由你决定,但这里有几个你可能还没有考虑过的"专业"理由。
Combine
是在系统层面上集成的。这意味着Combine
本身使用了未公开的语言特性,为你提供了你自己无法构建的API
。Combine
将许多常见的操作抽象为Publisher
协议上的方法,而且它们已经经过了良好的测试。- 当你所有的异步工作都使用相同的接口--
Publisher
--组合和重用性变得非常强大。 Combine
的操作符是高度可组合的。如果你需要创建一个新的操作符,这个新的操作符将立即与Combine
的其他部分即插即用。Combine
的异步操作符已经过测试。你所要做的就是测试你自己的业务逻辑。
正如你所看到的,大部分的好处都是围绕着安全性和便利性展开的。再加上该框架来自于苹果,投资编写Combine
代码看起来很有希望。
应用程序架构¶
由于这个问题很可能已经在你的头脑中敲响了警钟,所以请看一下使用Combine
会如何改变你原有的代码和应用架构。
Combine
不是一个影响你的应用结构的框架。Combine
处理的是异步数据事件和统一的通信合同--它不会改变,例如,你如何在你的项目中分离责任。
你可以在你的MVC(Model-View-Controller)
应用程序中使用Combine
,你可以在你的MVVM(Model-View-ViewModel)
代码中使用它,在VIPER
中使用,等等等等。
这是采用Combine
的一个关键方面,早期理解它很重要--你可以迭代地、有选择地添加Combine
代码,只在你希望改善代码库的部分使用它。这不是一个你需要做出的"全部或没有"的选择。
你可以先转换你的数据模型,或者调整你的网络层,或者只在你添加到你的应用程序的新代码中使用Combine
,同时保持你现有的功能不变。
如果你同时采用Combine
和SwiftUI
,情况就会略有不同。在这种情况下,从MVC
架构中删除C
确实是有意义的。但这要归功于Combine
和SwiftUI
的串联使用--这两者在同一个房间里,简直是火力全开。
视图控制器在面对Combine/SwiftUI
团队时没有任何机会。当你从数据模型到视图都使用反应式编程时,你就不需要一个特殊的控制器来控制你的视图。
如果这听起来很有趣,那你就有好戏看了,因为本书在第15章"Combine & SwiftUI实践"
中对这两个框架的共同使用做了扎实的介绍。
书中的项目¶
在本书中,你将首先从概念开始,然后转向学习和尝试众多的操作符。
与其他系统框架不同,你可以在Playground
这个孤立的环境中相当成功地使用Combine
。
在Xcode Playground
中学习可以使你很容易地向前推进,并在某一章节的进展中快速进行实验,并在Xcode
的控制台中即时看到结果。
Combine
不需要任何第三方的依赖,所以通常情况下,每一章的启动Playground
代码中包含的一些简单的帮助文件就足以让你运行。如果你在Playground
上做实验时Xcode
卡住了,快速重启可能会解决这个问题。
一旦你转向比玩一个操作符更复杂的概念,你将交替在Playground
和真正的Xcode
项目中工作,如Hacker News
应用程序,它是一个实时显示新闻的新闻阅读器。
重要的是,对于每一章,你都要从所提供的启动Playground
或项目开始,因为它们可能包括一些与学习Combine
无关的自定义辅助代码。这些花絮是预先写好的,这样你就不会分散自己对该章重点的注意力。
在最后一章中,你将利用你在全书中学到的所有技能,完成开发一个严重依赖Combine
和Core Data
的完整的iOS
应用。这将为你在用Combine
构建实际生活中的应用的道路上提供最后的推动力!
关键点¶
Combine
是一个声明式的、反应式的框架,用于处理随时间变化的异步事件。- 它的目的是解决现有的问题,如统一异步编程的工具,处理易变的状态,并使错误处理成为团队的起点。
Combine
围绕着三种主要类型:发布者在一段时间内发布事件,操作符异步处理和操作上游事件,订阅者订阅结果并用它们做一些有用的事情。
接下来去哪?¶
希望这一章的介绍是有用的,让你对Combine
解决的问题有一个初步的了解,并看看它提供的一些工具,使你的异步代码更安全、更可靠。
本章的另一个重要收获是对Combine
的期望,以及它的范围之外的东西。现在,你知道当我们说到反应式代码或异步事件时,你会遇到什么。当然,你也不要指望用Combine
来神奇地解决你的应用程序在导航或屏幕上的绘图问题。
最后,希望通过对即将到来的章节的了解,你能对Combine
和Swift
的反应式编程产生兴趣。向上,向前,我们来了!"。