前言
这是前段时间我在公司内部Android组的技术分享会上,以响应式编程为主题做的一个专题分享,反馈还不错,但是也有很多问题,因此我根据反馈重新修改和完善了相关的论述,组成一篇文章分享给大家。
研究这个问题的初衷在于目前很多人对于RxJava这种库,以及它背后所体现的编程思想了解不多,而网上也很少有人能够把它讲明白,很多时候只能参考网络上的一些RxJava项目实践去学习RxJava的使用。但是我始终认为,只有熟悉响应式编程的思想,才能更好的使用RxJava这个Rx拓展库。
目前网络上中英文的资料对于响应式编程的描述有些两极分化,要么只能将响应式的概念解释清楚,没有可实践性,要么就是从RxJava的定义出发来解释响应式编程。比如“响应式编程就是异步数据流编程”这种话,看似抓住了重点,但是实际上你很难从这个定义中收获有用的东西。
因此,今天我希望讲讲响应式编程的思想和它的优势,以及怎样去理解响应式编程才能更好的把它融入到我们的编程工作中,把响应式编程变成我们手中的利器。
响应式的由来
我们先来聊一聊响应式的由来,对于它的由来,我们可能需要先从一段常见的代码片段看起
int a=1;
int b=a+1;
System.out.print(“b=”+b) // b=2
a=10;
System.out.print(“b=”+b) // b=2
上面是一段很常见的代码,简单的赋值打印语句,但是这种代码有一个缺陷,那就是如果我们想表达的并不是一个赋值动作,而是b和a之间的关系,即无论a如何变化,b永远比a大1。那么可以想见,我们就需要花额外的精力去构建和维护一个b和a的关系。
而响应式编程的想法正是企图用某种操作符帮助你构建这种关系。
它的思想完全可以用下面的代码片段来表达:
int a=1;
int b <= a+1; // <= 符号只是表示a和b之间关系的操作符
System.out.print(“b=”+b) // b=2
a=10;
System.out.print(“b=”+b) // b=11
这就是是响应式的思想,它希望有某种方式能够构建关系,而不是执行某种赋值命令。
至此你可能不禁要问,我们为什么需要构建关系的代码而不是命令式的代码呢?如果你翻一翻自己正在开发的APP,你就能看到的每一个交互的页面其实内部都包含了一系列的业务逻辑。而产品的每个需求,其实也对应了一系列的业务逻辑相互作用。总之,我们的开发就是在构建一系列的业务逻辑之间的关系。你说我们是不是需要构建关系的代码?
说回响应式,前期由于真实的编程环境中并没有构建关系的操作符,主流的编程语言并不支持这种构建关系的方式,所以一开始响应式主要停留在想的层面,直到出现了Rx和一些其他支持这种思想的框架,才真正把响应式编程引入到了实际的代码开发中。
Rx是响应式拓展,即支持响应式编程的一种拓展,为响应式在不同语言中的实现提供指导思想
什么是响应式编程
说完了了响应式的由来,我们就可以谈谈什么是响应式编程了。
响应式编程是一种通过异步和数据流来构建事务关系的编程模型。这里每个词都很重要,“事务的关系”是响应式编程的核心理念,“数据流”和“异步”是实现这个核心理念的关键。为了帮助大家理解这个概念,我们不妨以APP初始化业务为例来拆解一下这几个词。
这是一个比较理想化的APP初始化逻辑,完成SDK初始化,数据库初始化,登陆,之后跳转主界面
事务的关系
-
事务
- 是一个十分宽泛的概念,它可以是一个变量,一个对象,一段代码,一段业务逻辑.....但实际上我们往往把事务理解成一段业务逻辑(下文你均可以将事务替换为业务逻辑来理解),比如上图中,事务就是指APP初始化中的四个业务逻辑。
-
事务的关系
- 这种关系不是类的依赖关系,而是业务之间实际的关系。比如APP初始化中,SDK初始化,数据库初始化,登陆接口,他们共同被跳转页面业务所依赖。但是他们三个本身并没有关联。这也只是业务之间较为简单的关系,实际上,根据我们的需求App端会产生出许多业务之间错综复杂的关系。
数据流
关于Rx的数据流有很多说法,比如“Everything is a stream”,“Thinking with stream”等等。虽然我明白这只是想强调流的重要性,可是这些话折射出来的编程思路其实是很虚无缥缈的,只会让开发者对于Rx编程更加迷惑。
实际上,数据流只是事务之间沟通的桥梁。
比如在APP初始化中,SDK初始化,数据库初始化,登陆接口这些业务完成之后才会去安排页面跳转的操作,那么这些上游的业务在自己工作完成之后,就需要通知下游,通知下游的方式有很多种,其中最棒的的方式就是通过数据(事件)流。每一个业务完成后,都会有一条数据(一个事件)流向下游,下游的业务收到这条数据(这个事件),才会开始自己的工作。
但是,只有数据流是不能完全正确的构建出事务之间的关系的。我们依然需要异步编程。
异步
异步编程本身是有很多优点的,比如挖掘多核心CPU的能力,提高效率,降低延迟和阻塞等等。但实际上,异步编程也给我们构建事务的关系提供了帮助。
在APP初始化中,我们能发现SDK初始化,数据库初始化,登陆接口这三个业务本身相互独立,应当在不同的线程环境中执行,以保证他们不会相互阻塞。而假如没有异步编程,我们可能只能在一个线程中顺序调用这三个相对耗时较多的业务,最终再去做页面跳转,这样做不仅没有忠实反映业务本来的关系,而且会让你的程序“反应”更慢
小结
总的来说,异步和数据流都是为了正确的构建事务的关系而存在的。只不过,异步是为了区分出无关的事务,而数据流(事件流)是为了联系起有关的事务。
APP初始化应该怎么写
许多使用Rx编程的同学可能会使用这种方式来完成APP的初始化。
Observable.just(context)
.map((context)->{login(getUserId(context))})
.map((context)->{initSDK(context)})
.map((context)->{initDatabase(context)})
.subscribeOn(Schedulers.newThread())
.subscribe((context)->{startActivity()})
其实,这种写法并不是响应式的,本质上还是创建一个子线程,然后顺序调用代码最后跳转页面。这种代码依然没有忠实反映业务之间的关系。
在我心目中,响应式的代码应该是这样的
Observable obserInitSDK=Observable.create((context)->{initSDK(context)}).subscribeOn(Schedulers.newThread())
Observable obserInitDB=Observable.create((context)->{initDatabase(context)}).subscribeOn(Schedulers.newThread())
Observable obserLogin=Observable.create((context)->{login(getUserId(context))})
.map((isLogin)->{returnContext()})
.subscribeOn(Schedulers.newThread())
Observable observable = Observable.merge(obserInitSDK,obserInitDB,obserLogin)
observable.subscribe(()->{startActivity()})
大家应该能很明显看到两段代码的区别,第二段代码完全遵照了业务之间客观存在的关系,可以说代码和业务关系是完全对应的。
那么这带来了什么好处呢?当initSDK,initDB,Login都是耗时较长的操作时,遵照业务关系编写响应式代码可以极大的提高程序的执行效率,降低阻塞。
理论上讲,遵照业务关系运行的代码在执行效率上是最优的。
为什么引入响应式编程
对响应式编程有了一些了解之后,我知道马上会由很多人跳出来说,不使用这些响应式编程我们还不是一样开发APP?
在这里我希望你理解一点,当我们用老办法开发APP的时候,其实做了很多妥协,比如上面的APP初始化业务,三个无关耗时操作为了方便,我们往往就放在一个线程环境中去执行,从而牺牲了程序运行的效率。而且实际开发中,这种类似的业务逻辑还有很多,甚至更加复杂。假如不引入响应式的思路,不使用Rx的编程模型,我们面对这么些复杂的业务关系真的会很糟心。假如你做一些妥协,那就会牺牲程序的效率,假如你千辛万苦构建出业务关系,最终写出来的代码也一定很复杂难以维护。所以,响应式编程其实是一种更友好更高效的开发方式。
根据个人经验来看,响应式编程至少有如下好处:
- 在业务层面实现代码逻辑分离,方便后期维护和拓展
- 极大提高程序响应速度,充分发掘CPU的能力
- 帮助开发者提高代码的抽象能力和充分理解业务逻辑
- Rx丰富的操作符会帮助我们极大的简化代码逻辑
一个复杂一些的例子
接下来,我就以我们团队目前的一款产品的页面为例,详细点介绍运用响应式编程的正确姿势。
首先,UI和产品沟通后,可能会给我们这样的设计图(加上一些尺寸的标注)。但是我们并不需要急忙编码,我们首先要做的是区分其中相对独立的模块。
上图我做了一点简单的标注。把这个页面的业务逻辑简单的分为四个相互独立的模块,分别是视频模块,在线人数模块,礼物模块,消息模块。他们相互独立,互不影响。接下来,我们再去分析每个模块内部的业务并构建起业务之间的关系。大致如下:
构建了业务之间的关系图,其实我们的工作已经完成了一半了,接下来就是用代码实现这个关系图。在这里,我就以其中一小段业务关系来编写代码给大家示范。
Observable obserDownload=Observable.just(url)
.map((url)->{getZipFileFromRemote(url)});
Observable obserLocal=Observable.just(url)
.map((url)->{getZipFileFromLocal(url)});
Observable obserGift=Observable.concat(obserLocal,obserDownload)
.takeUnitl((file)->{file!=null});
obserGift.subscribeOn(Schedulers.io()).flatMap((file)->{readBitmapsFromZipFile(file)})
.subscribe((bitmap)->{showBitmap(bitmap)})
以上是我手写的伪代码,可能细节上有些问题,但大体思路就是这样。
有人可能会说,那是因为你运用操作符比较熟练才能这么写。其实操作符都是我查的,我记不住那么多操作符,所以基本上我都是先理清楚业务之间的关系,需要和并逻辑的时候,就去去查合并类的操作符,需要条件判断来分流的逻辑时去找条件判断类的操作符。基本上都能满足需求。你瞧,写代码就是这么简单,后续即使需要增加需求,代码修改起来也很清晰,因为无关的业务已经被你分离好了。
所以,赶紧在你的项目中引入响应式编程吧!
勘误
暂无
后记
前一段时间换了工作,在目前的团队里除了实现日常需求,也会负责项目重构这一块,我会更多的使用响应式的方式重构项目,同时也会尽力在团队内部推动响应式编程的使用,一旦有了新的体会,我也会在第一时间和大家分享。
由于这篇文章讲的是响应式编程,因此更多的使用的Rx这个名称,而不是RxJava,因为RxJava只是响应式编程在Java语言中的实现。不过里面的伪代码都是使用RxJava来编写的,希望大家能够理解。
假如你对RxJava不是十分了解的话,欢迎大家去看我之前写的Rxjava系列文章