【编者的话】响应式编程掀起了Swift的革命,那么它的背后是什么呢?Rx的神秘面具下又是什么呢?最近 ReactiveX / RxSwift项目 参与者Junior Bontognali在他的博客上发表了一篇文章 The Reactive Revolution of Swift ,介绍了自己对响应式编程原理的理解,帮助我们掀开了响应式编程和Rx的神秘面纱,本文便是由该文翻译整理而来。
我曾讲过RxSwift这个令人惊叹的抽象概念/框架,以及它可以怎样帮我们简化处理异步编程。这里我讲一下响应式编程(Reactive Programming)是如何将异步编程推到一个全新高度的。
大多数有关响应式编程的演讲和文章都是在展示Reactive框架如何好如何惊人,给出一些在非常复杂的情况下,只需几行代码就可以搞定的例子。例子么?我这里有一段 基于RxSwift的聊天程序的代码 :
socket.rx_event .filter({ $0.event == "newMessage" && $0.items?.count > 0}) .map({ Array($0.items!) }) .subscribeNext { data in let username = data[0]["username"] as? String ?? "unknown" let text = data[0]["message"] as? String ?? "invalid text" let message = JSQMessage(senderId: username, displayName: username, text: text) self.messages += [message] self.finishSendingMessage() }.addDisposableTo(disposeBag)
这段代码展示了socket事件是如何被过滤和处理,以显示某个特定的用户在聊天中发送的消息的。经典的方法可能会以类似下面的代码作为结束:
dispatch_async(dispatch_get_global_queue()) { let socketData = self.loadDataFromSocket() let data = self.parseData(data) dispatch_async(dispatch_get_main_queue()) { let username = data[0]["username"] as? String ?? "unknown" let text = data[0]["message"] as? String ?? "invalid text" let message = JSQMessage(senderId: username, displayName: username, text: text) self.messages += [message] self.finishSendingMessage() } }
这就是所谓的“回调地狱”,代码难以阅读和维护。但是除了真的很难阅读外,回调为什么这么不好呢?
同步编程不仅仅是在一个单独的线程中运行任务或者执行计算,在某些时候,我们需要在多个线程中同步数值,而最常见的解决方法就是添加锁。一旦锁被引入,代码的复杂性就至少提高了一个数量级,而且又引入了一个新问题:不可预测性。
在计算方面,代码现在变得不可预测。当一个单一线程被调度时,以及假如我们访问一个预期的单一(已上锁的)属性值时,没有办法可以确定地知道我们是否会丢掉值或者正在处理和之前相同的值。
那么异步编程的真正困难是什么呢?答案是同步。听起来很有意思,但这是真的。
响应式编程不是我们很多人所想的那样是一个年轻的概念,这一点很重要。它的起源可以追溯到1969年,计算机科学的传奇人物Alan Kay在犹他大学的那篇名为“ The Reactive Engine ”的博士学位论文。但我不是在这里给大家上历史课的,考虑到有些概念听起来很好,实验起来却没那么好用,所以让我们看看响应式编程在处理同步代码时所体现出的价值。
在 Rx 的世界中,最根本的部分是 观察者模式 (Observer pattern)和 迭代器模式 (Iterator pattern)的结合,两者都是众所周知的模式,而且在编写软件时被广泛用于处理一些特定的行为。观察者模式和迭代器模式,这两者是相互作用的,在两种情况下的计算的都是从生产者中取出的值。对于迭代器模式,只要他们是可用的,我们就可以获取值,对于观察者模式,我们是在生产者给所有具体的观察者发出通知后,从中获取数据去处理值。获取绝对是一个很好的既定具体解决方案,只要一切都是在同一线程中处理的,它就能很好地起作用。当数据被获取出来,进行一步的处理时会发生什么呢?好了,此时我们添加的锁开始发挥作用了,事情很快就难办了。
我们设想一下,有一个非常好的应用,已经准备好了在几天内发布,但是在最后的测试中,在某些情况下,一些死锁和征用条件产生了,而应用的崩溃是随机的,只有足够的数据才能够去确定这个问题。时间是有限的,问题在可能出现的地方比预期的还要发生得更快,但是解决方案或许不能那么简单和快速地开发和部署出来。首先要记住的是,一旦采用了锁定策略,那么整个代码的运行速度都会减慢,同时异步编程的不可预测性也会影响到一般应用程序的性能。也有可能会有数量级的增长放缓,我们可以得到的结论是:这是不可接受的。
响应式是一种设计,获取数据是一个绝对的东西,可能很难在很短的一段时间内调整,所以一个解决方案是翻转该行为,即为什么不能让资源自己把值推送给一个用户/消费者,而是我们去从一个资源获取呢?这就是Rx具体的内容,即推送数据给被生产者订阅了的实例。我们在某些地方产生数据,然后根据需求将数据推送给用户并进行处理。
观察者 + 迭代器 + 推送 = 可观测的(Observable)实体
Rx背后的数学很简单,两个非常确定的实体,结合不同的交互模型,成为了一个可观测的实体的根基。这就是革命发生的地方,结合旧的,既定的概念,以一定的方式创建和模拟一个强大的抽象概念,来帮助处理异步编程,而不用去冒着项目在最后一周挂掉的危险。
这个数学公式的结果是一个叫做可观测的实体,这个对象负责处理原始数据,并在必要时将值推送给用户。用户可以扮演多种角色,它们可以成为别的连锁的可观测实体、操作者或者仅仅是回调。Rx是非常基本的,但也是非常强大的抽象概念。
使用可观测实体的方法要求初始化可观测实体时声明逻辑,这意味着我们的代码变得更加紧凑,而且通常被限制在view Controller的初始化方法中。在这个的RxChat的例子中,逻辑的大部分都是在viewDidLoad的函数中声明的:
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.setup() user .distinctUntilChanged() .subscribeNext() { username in self.senderId = username self.senderDisplayName = username self.socket.emit("addUser", withItems:[username]) }.addDisposableTo(disposeBag) socket.rx_event .filter({ $0.event == "connect"}) .map({ _ in return () }) .subscribeNext { _ in self.title = "RxChat (Connected)" }.addDisposableTo(disposeBag) // [...] }
这种方法也被称为声明式编程(Declarative Programming),可以帮助我们有效减少潜在bug的数量,也使得在使用MVVM或者类似的模式时所写出的代码让人眼前一亮。
一般来说,学习Rx或响应式编程就像学习一门新的语言(不是指编程语言),步骤是非常相似的,我们在最初的阶段学习基本语法和常用句子,然后学习规则和语义,最后我们能够说出某些甚至最难的话题时,我们就掌握了这门语言。
为了持续学习,我建议读者阅读以下资料:
这篇文章可能看起来非常短(事实上,我也打算把它写这么短),但是它写出了我心中关于Rx的基础知识的理解。大部分的文章和演讲都是在外围讨论如何使用它,为什么要用它,但是很少有人讨论为什么Rx是这样的强大,为什么它可以这么短。Rx的面具下没有魔法,RxSwift所用到的都是一些既有的东西,它只是建立概念,用聪明的方法将这些东西粘在一起,来创建一个强大的异步计算的抽象概念。不要再花时间给你的异步软件写同步策略了,把时间花在写逻辑上吧。
NetNewsWire的创始人,Vesper的开发者,现就职于Omni Group的Brent Simmons对本文中Junior Bontognali的解决方案提出质疑,并撰写了一篇文章“ The Non-Reactive Solution ",给出了不用响应式编程来解决示例问题的方案。
Simmons认为,Bontognali称“在后台队列加载并解析数据,然后回调到主线程并更新消息队列和UI”是“回调地狱”没有问题,但这一处理方法明显是错误的。他认为,一个View Controller无论在何时发送网络请求都是错误的,而Bontognali所提出的响应式编程的方案并没有避免这个问题。
为此,Simmons提出了他自己的解决方案:首先,设置一个独立的network Controller,由它去读取和解析数据,view Controller在需要读取或解析数据时。告诉network Controller去做相应的处理即可。而network Controller完成了相应的工作后,可以在主线程中发送通知来告知view Controller。在收到通知后,view Controller再去更新UI。
Simmons认为,在他提出的解决方案中,没有用到锁,所以也不存在Bontognali所提出的后续问题。同时,Simmons也表示,自己并不是说响应式编程或RxSwift不好,只是指出了文中的不妥之处。