这本书的目的是向读者介绍 RxSwift 库,以及如何使用 Swift 编写响应式 iOS 应用程序。
“但什么是 RxSwift 呢?” 这里有一个很好的定义:
RxSwift 是一个库,通过使用可观察序列和函数式操作符来组合异步和基于事件的代码,允许通过调度程序进行参数化执行。
听起来复杂? 如果是这样,也不用担心。编写响应式程序,理解程序背后的许多概念,浏览大量相关的、常用的术语,这些都是令人生畏的——尤其是当你试图一次把所有内容都看完,或者当你还没有以一种结构化的方式被介绍给它的时候。
这就是本书的目标:通过解释如何使用每一个 API,并介绍它们在 iOS 应用程序中的实际应用,逐步向你介绍各种 RxSwift API 和 Rx 概念。
你将从 RxSwift 的基本特性开始,然后逐步学习中级和高级主题。在学习过程中,花时间广泛地练习新概念,这将使你在本书的最后更容易掌握 RxSwift 。Rx 是一个太广泛的主题,不能完全涵盖在一本书里;相反,我们的目标是让你对它有一个坚实的了解,这样你就可以继续发展自己的 Rx 技能。
我们还没有完全确定什么是 RxSwift ,是吗? 让我们先从一个简单易懂的定义开始,然后在本章稍后的内容中讨论响应式编程时,再进一步讨论一个更好、更有表现力的定义。
RxSwift 本质上简化了异步程序的开发,它允许代码对新数据做出反应,并以顺序的、隔离的方式对其进行处理。
作为 iOS 应用程序开发人员,与本章前面阅读的第一个定义相比,这应该更清楚,也更能告诉你什么是 RxSwift。
即使你仍然不清楚细节,也应该清楚 RxSwift 帮助你编写异步代码。而且你知道开发好的、确定性的、异步的代码是很困难的,所以任何帮助都是非常受欢迎的!
异步编程介绍(Introduction to asynchronous programming)
如果你试图用一种简单的、实际的语言来解释异步编程,你可能会得到以下内容。
iOS应用程序在任何时候都可能会做以下任何事情,甚至更多:
- 对按钮点击做出反应
- 在文本字段失去焦点时设置键盘动画
- 从网上下载一张大图片
- 将数据保存到磁盘
- 播放音频
所有这些事情似乎同时发生。每当键盘在屏幕外显示动画时,应用程序中的音频在动画完成之前不会暂停,对吗?
你的程序的所有不同部分都不会互相阻止对方的执行。iOS 提供了各种 API ,允许你在不同的线程上执行不同的工作,并在设备 CPU 的不同核心上执行它们。
然而,编写真正并行运行的代码相当复杂,尤其是当不同的代码位需要处理相同的数据块时。很难说哪段代码首先更新数据,或者哪段代码读取最新值。
Cocoa 和 UIKit 异步API(Cocoa and UIKit asynchronous APIs)
苹果在 iOS SDK 中提供了很多 API 来帮助你编写异步代码。你在自己的项目中使用过这些功能,但可能从来没有考虑过,因为它们是编写移动应用程序的基础。
你可能已经使用了以下大部分:
- 通知中心(NotificationCenter):在发生感兴趣的事件时执行一段代码,如用户更改设备的方向或屏幕上显示或隐藏的软件键盘。
- 委托模式(The delegate pattern):允许你定义一个代表另一个对象或与另一个对象协作的对象。例如,在你的 APP 委托中,你定义了当一个新的远程通知到达时会发生什么,但是你不知道这段代码什么时候会被执行,或者它会被执行多少次。
- GCD(Grand Central Dispatch):帮助你抽象执行工作片段。你可以在串行队列中调度要顺序执行的代码,或者在具有不同优先级的不同队列上并发运行大量任务。
- 闭包(Closures):创建可以在代码中传递的分离代码段,以便其他对象可以决定是否执行、执行多少次以及在什么上下文中执行。
由于大多数典型代码都会异步执行一些工作,而且所有 UI 事件本质上都是异步的,所以不可能假设整个应用程序代码执行的顺序。
毕竟,应用程序的代码运行方式取决于各种外部因素,比如用户输入、网络活动或其他OS事件。每次用户启动应用程序时,根据这些外部因素,代码运行的顺序可能完全不同。
我们并不是说编写好的异步代码是不可能的。毕竟,上面列出的苹果的 API 非常先进,非常专业,公平地说,与其他平台相比,它们的功能非常强大。
问题在于,复杂的异步代码变得非常难以编写,部分原因在于苹果的 SDK 提供了多种 API :
使用代理要求你采用一种特定的模式,另一种模式用于闭包,还有另一种方法用于订阅 NotificationCenter ,等等。由于所有异步 API 之间没有通用语言,因此阅读和理解代码并对其执行进行推理变得非常困难。
为了结束本节并将讨论放到更大的上下文中,你将比较两段代码:一段同步代码和一段异步代码。
同步代码(Synchronous code)
为数组的每个元素执行操作是你已经做过很多次的事情。这是一个非常简单但坚实的应用程序逻辑构建块,因为它保证了两件事:它同步执行,当你在其上迭代时,集合是不可变的。
花点时间想想这意味着什么。当你遍历一个集合时,你不需要检查所有元素是否仍然存在,也不需要回滚,以防另一个线程在集合的开头插入一个元素。假定总是在循环开始时对集合进行整体迭代。
如果你想在 for 循环的这些方面玩得更多一些,可以在 playground 上试试:
var array = [1, 2, 3]
for number in array {
print(number)
array = [4, 5, 6]
}
print(array)
array
在for
循环体内是否可变?循环迭代的集合是否发生更改?所有命令的执行顺序是什么?如果需要,你能修改number
吗?
异步代码(Asynchronous code)
考虑类似的代码,但是假设每次迭代都是对按钮点击的反应。当用户反复点击按钮时,应用程序就会打印出数组中的下一个元素:
var array = [1, 2, 3]
var currentIndex = 0
// This method is connected in Interface Builder to a button
@IBAction func printNext(_ sender: Any) {
print(array[currentIndex])
if currentIndex != array.count - 1 {
currentIndex += 1
}
}
请在与前面代码相同的上下文中考虑这段代码。当用户点击按钮时,会打印出数组的所有元素吗?你真的不好说。
另一段异步代码可能会在打印之前删除最后一个元素。或者,在继续之后,另一段代码可能会在集合的开头插入一个新元素。此外,你假定只有printNext(_:)
会更改currentIndex
,但是另一段代码也可能修改currentIndex
——也许是在创建了上述函数之后添加的一些“聪明的”代码。
你可能已经意识到编写异步代码的一些核心问题是:
- 执行任务的顺序
- 共享可变数据
幸运的是,这是 RxSwift 的一些强项!
接下来,你需要一个很好的入门语言,这将帮助你开始理解 RxSwift 是如何工作的,以及它解决了哪些问题;这最终会让你跳过这个温和的介绍,在下一章中编写你的第一个 Rx 代码。
异步编程术语表
RxSwift 中的一些语言与异步、响应式或函数式编程紧密地绑定在一起,如果你首先理解以下基本术语,就会更容易理解。
总的来说,RxSwift 试图解决以下问题:
1.状态,特别是共享可变状态
状态有点难以定义。要理解状态,请考虑下面的实际示例。
当你启动你的笔记本电脑时,它运行得很好,但是,在你使用它几天甚至几周后,它可能会开始表现得古怪或突然挂起,拒绝和你说话。硬件和软件保持不变,但改变的是状态。一旦重新启动,相同的硬件和软件组合将再次正常工作。
内存中的数据、存储在磁盘上的数据、对用户输入做出反应的所有控件、从云服务获取数据后留下的所有痕迹——这些总和就是你的笔记本电脑的状态。
管理应用程序的状态,尤其是在多个异步组件之间共享时,是本书中你将学习如何处理的问题之一。
2. 命令式编程(Imperative programming)
命令式编程是一种使用语句改变程序状态的编程范式。就像你在和你的狗狗玩耍时使用命令语言一样——“去拿回来!躺下!装死!” - 你使用命令式代码告诉应用程序确切的时间和如何做事情。
命令式代码与计算机能够理解的代码类似。CPU 所做的就是遵循冗长的简单指令序列。问题是,对于人类来说,为复杂的异步应用程序编写命令式代码非常具有挑战性——尤其是涉及到共享可变状态时。
例如,以在 iOS 视图控制器的 viewDidAppear(_:)
中找到的代码为例:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupUI()
connectUIControls()
createDataSource()
listenForChanges()
}
目前还不知道这些方法有什么用。它们是否更新视图控制器本身的属性?更令人不安的是,它们的调用顺序是否正确?也许有人无意中交换了这些方法调用的顺序,并将更改提交给了源代码管理。现在,由于调用顺序的交换,应用程序的行为可能会有所不同。
3.副作用(Side effects)
既然你对可变状态和命令式编程有了更多的了解,你就可以把这两件事的大部分问题归结为副作用。
副作用表示代码当前作用域之外状态的任何更改。例如,考虑上面例子中的最后一段代码。connectUIControls()
可能将某种事件处理程序附加到某些 UI 组件。这会产生一个副作用,因为它改变了视图的状态:应用程序在执行connectUIControls()
之前以一种方式执行,之后以另一种方式执行。
任何时候修改存储在磁盘上的数据或更新屏幕上标签的文本,都会产生副作用。
副作用本身并不是坏事。毕竟,产生副作用是任何程序的最终目标!你需要在你的程序执行完毕后以某种方式改变世界的状态。
运行一段时间,什么都不做,会让应用程序变得非常无用。
产生副作用的一个重要方面是用一种可控的方式。你需要能够确定哪些代码片段会导致副作用,以及哪些代码片段只处理和输出数据。
RxSwift 试图通过处理以下几个概念来解决上面列出的问题。
4.声明式的代码(Declarative code)
在命令式编程中,你可以随意改变状态。在函数式编程中,您的目标是最小化引起副作用的代码。既然你没有生活在一个完美的世界里,那么平衡就在两者之间。RxSwift 结合了命令式代码和函数式代码的一些最佳方面。
声明性代码允许你定义行为片段。只要有相关事件,RxSwift 就会运行这些行为,并提供一个不可变的、独立的数据块来处理。
通过这种方式,你可以处理异步代码,但是要做出与简单的 for 循环相同的假设:你正在处理不可变数据,并且可以以顺序的、确定的方式执行代码。
5.响应式系统
响应式系统是一个相当抽象的术语,涵盖了 web 或 iOS 应用程序,这些应用程序大多或全部具备以下品质:
- 响应性:始终保持UI最新,表示最新的应用程序状态。
- 可恢复:每个行为都是独立定义的,提供了灵活的错误恢复。
- 灵活性:代码处理不同的工作负载,通常实现诸如延迟下拉式数据收集、事件节流和资源共享等特性。
- 消息驱动:组件使用基于消息的通信来改进可重用性和隔离性,解耦类的生命周期和实现。
既然你已经很好地理解了 RxSwift 帮助解决的问题以及它是如何处理这些问题的,那么现在就可以讨论 Rx 的构建块以及它们如何协同工作了。