引言
RxAndroid是一个开发库、是一种代码风格、也是一种思维方式。
正如标题所言,RxAndroid的特点是简洁、优雅、高效,它的优点是多线程切换简单、数据变换容易、代码简洁可读性好、第三方支持丰富易于开发;缺点是学习成本较高、出错难以排查。
本文参考了“扔物线”的文章:给 Android 开发者的 RxJava 详解
本文参考了“C.L. Wang”的文章:详细解析 RxAndroid 的使用方式
本文参考了“张磊BaronZhang”的系列文章:RxJava系列
本文参考了“small-dream”的文章:Retrofit基本使用教程
用途与优势
·起源
RxAndroid源于RxJava——"a library for composing asynchronous and event-based programs using observable sequences for the Java VM“,意为“一个在Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库”。
·用途
RxAndoid在我的粗略理解中,是一个实现异步操作的库,具有简洁的链式代码,提供强大的数据变换。
在详细解析 RxAndroid 的使用方式中,有这样一个例子:
这个界面是一个典型的Android图文列表,取在线数据,并显示为图文列表,业务流程是这样的:
1.输入多个人名
2.根据人名,在线查询对应的信息
3.获取返回信息,显示为图文列表
我们在常规开发中,一般要做到这样几件事:
•用异步线程在线查询数据
•用foreach查询每个用户(没有批量查询的接口)
•访问网络,查询每个人名对应的github信息(返回Json字符串)
•类型转换(Json->Java Object)
•在主线程中接收数据
•每收到一条数据,就用Adapter向列表添加一条,并刷新界面
实现这样的需求,可以说功能不多,代码不少,少说也得N个类+几百行代码吧,加班到23:59:59应该就能搞定了…工作量主要在异步网络操作、类型转换和界面刷新上。
不过,RxAndroid可以做到,用区区一小段儿代码就实现这个需求:
除了MainActivity、layout文件和Adapter类之外,主要的业务逻辑都在这一段儿代码中实现了。感兴趣的朋友可以在详细解析 RxAndroid 的使用方式中找到这段儿代码。
刚才谁说要几百行代码来着...
·优势
我看到这段代码,想到的第一件事就是异步好简单、代码好简洁,一个简单、一个简洁,这就意味着工作效率。
知识结构
前面说过,RxAndroid学习成本比较高,其中一个原因是出现的概念和知识点比较多,我个人倾向于把它们分为三部分:基本概念、线程控制和变换操作。
��·基本概念
先看一下刚才那段儿RxAndroid代码,里面有这么几个元素:
图中出现的基本概念,包括观察者、被观察者和订阅、另外还有一个图中没有出现的事件。
线程控制和变换操作我们先略过,在后面再看。
我们理解一个观察者、被观察者、订阅和事件的关系:
这显然是一个观察者模式,其中有四个基本概念:Observable (可观察者,即被观察者)、 Observer (观察者)、 subscribe (订阅)、事件。Observable 和Observer 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出事件来通知 Observer。
与传统观察者模式不同, RxJava 的事件回调方法除了普通事件 onNext() (相当于 onClick() / onEvent())之外,还定义了两个特殊的事件:onCompleted() 和 onError(),作为事件队列的结束。
·基本概念—被观察者Observable
被观察者有这么几个特点:
•每一段Rx代码都从被观察者开始
•被观察者向观察者抛出onNext,onCompleted,onError事件,onNext可以连续多个,但onCompleted/onError只能出现一次,且出现后就结束事件队列
•用一些操作符可以方便地生成Observable对象,例如这段代码等价于Observable.just("Hello","Hi","Aloha");
·基本概念—观察者Observer及其变体
观察者有这么几个特点:
•被观察者抛出事件,观察者响应事件
•观察者决定如何响应onNext/onCompleted/onError事件
•订阅(.subscribe(...))的逻辑和写法是有问题的,从逻辑上是观察者订阅被观察者,但写法却好像是被观察者订阅了观察者,这是为了不打断Rx的链式代码格式,而且,我个人觉得写成.subscribeby(...)更恰当
观察者Observer有多种变体的写法:
.subscribe(new Observer…)
.subscribe(new Subscriber…)
.subscribe(new onNextAction…,new onErrorAction…,new onCompleteAction…)
.subscribe(new onNextAction…,new onErrorAction…)
.subscribe(new onNextAction…)
其中,Observer是原始写法;
Subscriber继承了Observer,是推荐写法;
也可以直接传对三个事件的响应;
或者只响应两个事件、或者只响应一个事件。
ActionN和FuncN
我们注意到,上图的代码中出现了ActionN和FuncN的代码形式,这都是RxJava的接口:
ActionN,Action0/Action1/…ActionN是RxJava的接口,可传入0~N个参数,没有返回值;
FuncN,从Func0/Func1/…FuncN是RxJava的接口,可传入0~N个参数,有返回值。
到这里,其实我们就已经能读懂大部分的Rx代码了,包括最前面那段儿。
·基本概念—订阅、取消订阅、延迟
RxAndroid在订阅和取消订阅时也有丰富的处理函数:
•doOnSubscribe,在subscribe()调用后&事件发送前,执行,可以指定线程
observable.doOnSubscribe(…)
•doOnUnsubscribe,取消订阅时的监听
observable.doOnUnsubscribe(…)
•doOnCompleted,正常结束时的监听
observable.doOnCompleted(…)
•doOnError,出错时的监听
observable.doOnError(…)
•doOnTerminate,即将被停止(正常/异常)监听
observable.doOnTerminate(…)
•finallyDo,结束(正常/异常)监听
observable.finallyDo(…)
•delay,延迟一段儿时间再发射(已运算)
observable.from(items).delay(5,TimeUnit.SECONDS).observeOn(Schedulers.newThread()).subscribeOn(Schedulers.newThread());
•delaySubscription,延迟onNext事件
observable.delaySubscription(2,TimeUnit.SECONDS)
•timeInterval,定制发射事件
observable.interval(1,TimeUnit.SECONDS).take(5).timeInterval();
•要及时释放订阅,否则可能造成内存泄漏
·基本概念—事件
就是刷一下事件这个概念的存在感,本节无内容...
·线程控制
RxAndroid的一个突出优势是多线程控制非常简单。
·线程控制—跨线程
和以往代码繁琐的多线程控制不同,RxAndroid可以轻松地用一行代码来切换线程
两个函数:
•subscribeOn()指定事件产生的线程(为前面的代码指定线程)
•observeOn()指定事件消费的线程(为后面的代码指定线程)
�多钟线程:
•Schedulers.immediate,默认,当前线程,立即执行
•Schedulers.trampoline,当前线程,要等其他任务先执行
•Schedulers.newThread,启用一个新线程
•Schedulers.io,擅长读写文件、数据库、网络信息,一个无数量上限的线程池
•Schedulers.computation,擅长cpu密集型计算,固定线程池,不要做io操作,会浪费cpu
•AndroidSchedulers.mainThread,Android主线程
·线程控制—如何操作
我们举个栗子,说明如何灵活地切换线程
需要注意的是:
•subscribeOn只能定义一次,除非是在定义doOnSubscribe
•observeOn可以定义多次,决定后续代码所在的线程
·线程控制—安全性
只要涉及到多线程,就一定要考虑安全性问题,这方面,我个人主要考虑3种方法
使用Rx缓存:
被观察者可以使用Rx缓存,对同一个被观察者对象执行订阅或取消订阅时,这个被缓存的Observable不会去重复查询数据,它会按自己的节奏去继续执行网络请求,你需要在生命周期之外去做存储。
使用RxLifecycle:
及时取消订阅:
·变换操作
除了前面讲的多线程操作简单,Rx还有一个深受广大群众喜爱的特点,就是强大的数据变换能力。
所谓变换,就是将事件序列中的对象或整个序列进行加工处理,转换成不同的事件或事件序列
一个简单的例子:
实际上,变换有两大类,一类是针对事件/数据的lift变换,另一类是针对Observable本身的compose变换:
·变换操作—lift变换
我们先看三个典型的lift变换函数:
flatmap函数,可以把把一组String转换为一组User数据对象。
throttlefirst函数,在连续点击时,在两秒内只响应第一个点击事件。
merge函数,可以把两组数据合并为一组数据。
上述3个函数,代表三种lift变换操作符:
转换操作符:能实现一对一转换、一对多转换、依次将输出作为输入、分组输出 等
常用函数包括 map flatMap concatMap flatMapIterable switchMap scan groupBy ...
过滤操作符:能实现去重、取前N个、取后N个、取第N个、从第N个取、自定义过滤 等
常用函数包括 fileter take takeLast takeUntil distinct distinctUntilChanged skip skipLast ...
组合操作符:能实现数据合并、数据运算、数据排列、组合配对 等
常用函数包括 merge zip join combineLatest and/when/then switch startSwitch...
·变换操作—compose变换
compose变换不针对事件和序列,针对的是Observable本身,可以理解为,compose是定义一组变换模板,让多个Observable执行同样的变换
一个典型的例子是,用compose函数实现与RxActivity生命周期的绑定:
注意,这里绑定的是RxActivity的生命周期,Activity本身是没有这个功能的。
·回顾一下lift和compose变换的区别
lift变换,针对事件项和事件序列
lift的功能在于变换数据,定义如何响应事件:
·变换、如String->Bitmap;
·过滤、如只处理前三条;
·合并、如把两组数据合并为一组
compose变换,针对Observable
compose的功能在于定义被观察者的行为:
定义一组变换模板,让多个Observable实现同一组变换
如:设置Observable的生命周期
变换是RxJava中非常重要,也比较复杂的一部分内容,这里只是做了简单的归纳和介绍,建议仔细阅读给 Android 开发者的 RxJava 详解和RxJava系列,对变换建立更深入更全面的了解。
·回顾知识结构
基本概念里,我们主要了解了基于事件订阅的开发风格
线程控制里,我们了解到如何简洁高效地操作异步线程,决定每一段儿代码运行的线程
�变换操作里,我们了解到RxAndroid如何使用强大的数据查询/变换/使用
相关开发库
�在使用RxAndroid的开发中,可选择很多Rx特性的开发库
Lambda表达式
Java8的特性,可以进一步简化代码,但暂无Android Studio支持,需要retrolambda插件,有兼容性问题,可读性和可维护性差,风险高,目前不建议使用
RxBinding
处理控件的UI事件,提供一些高级事件,普通如点击、长按、勾选,高级如防抖(throttleFirst,N秒内只允许点击一次)、等待(debounce,一定时间内没有操作就触发事件)
RxLifecycle
设置生命周期,如自动跟随Activity或Fragment的生命周期去创建/销毁,不用自己写线程,但要使用Rx的Activity和Fragment
Retrofit
最匹配RxAndroid的网络访问框架,支持Rx的链式调用,可直接返回数据对象,自定义特性也非常强大
SqlBrite
帮你用Rx的方式完成数据查询,但它不是一个数据库框架
·相关开发库—Labmbda表达式
Lambda与Rx融合得更好,能使代码更简洁(可是…再等等吧)
·相关开发库—RxBinding
实质是用Observable去包装UI事件,然后就可以用RxJava的各种转换功能去扩展UI事件
�·相关开发库—RxLifecycle
通过compose操作符调用,完成Observable和当前的UI组件绑定,实现生命周期同步。当UI组件生命周期结束时,就自动取消对Observable订阅。
bindToLifecycle的生命周期与调用的位置有关:
��·相关开发库—Retrofit
一段儿常规的Retrofit代码,包括接口文件、初始化和调用:
在这个例子中,Retrofit拼url的过程如下:
•定义基础url:"https://api.github.com/"
•定义@GET的参数:"users/{user}"
•定义函数的入参:@Path("user") String username
•基础url+@GET中的url:"https://api.github.com/users/{user}“(如果@GET的参数是另一个url如http://www.google.com/users/{user},就可以替换掉原来的基础url)
•根据参数@Path的传值,更换user的值:"https://api.github.com/users/jake“
Tips: 基础url不能写成"https://api.github.com"(必须以“/”结尾)
上述代码是void函数,用callback返回数据,Retrofit还可以直接返回Observable对象:
上述例子均为GET方式,Retrofit还可以向服务器提交POST数据:
因为POST提交Json需要加消息头"Content-type","application/json“,因为Retrofit默认使用的是okhttp这个网络框架,所以需要设置okhttp的拦截器Intercepter,拦截http请求进行监控,重写或重试:
下面大概列举一下Retrofit各种网络操作的写法:
需要注意的是,Retrofit使用okhttp实现网络访问,而okhttp在Android主线程执行时会报错,所以要注意网络操作所在的线程。
建议阅读Retrofit基本使用教程,详细了解一下Retrofit这个网络框架
��·相关开发库—SqlBrite
如上图所示,SqlBrite使用Rx的订阅模式来查询 ,而不是直接执行查询,SqlBrite不是一个数据库框架,他依然需要让你自己写sql语句来创建数据库查询,就是查询的等操作,采用了RX的方式来完成,这样的好处,就是我们在展示列表的时候,增删改查后,Sqlbrite都会自动的调用查询,来改变你传入列表的数据。
实例分析
·实例分析-改造目标
我手头有一个项目,准备用RxAndroid进行改造,并评估改造效果,我希望做到以下几点:
•改造一个自定义控件,使用Rx的Interval来替换线程,使用RxLifeCycle把取消订阅托管给UI组件
•把原来的多层数据访问及处理改用Rx的链式方式实现
•用Retrofit实现Rx风格的网络访问
•保留MVP模式
•简化代码,增强代码可读性
·实例分析-环境准备
添加gradle引用:
compile'io.reactivex:rxandroid:1.1.0' //
RxAndroid
compile 'io.reactivex:rxjava:1.1.0' //推荐同时加载RxJava
compile'com.squareup.retrofit:retrofit:2.0.0-beta2' // Retrofit网络处理
compile'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2' // Retrofit的rx解析库
compile'com.squareup.retrofit:converter-gson:2.0.0-beta2' // Retrofit的gson库
compile 'com.trello:rxlifecycle:0.4.0' //RxLifecycle管理Rx的生命周期
compile'com.trello:rxlifecycle-components:0.4.0' // RxLifecycle组件
// RxBinding
compile'com.jakewharton.rxbinding:rxbinding:0.3.0'
compile'com.jakewharton.rxbinding:rxbinding-appcompat-v7:0.3.0'
compile'com.jakewharton.rxbinding:rxbinding-design:0.3.0'
·实例分析-一个自定义控件
我有一个自定义的播放控件,功能如下:
准备使用Rx的Interval来替换线程,使用RxLifeCycle把取消订阅托管给UI组件
改造前后的代码如下:
用Rx的Interval代替原有的线程操作
用RxLifeCycle来实现与RxActivity的生命周期同步
改造后的效果:
•工作量大为减少
•不需要复杂的自定义线程
•代码变简洁易读,没有了迷之缩进,业务逻辑呈链式
•提高了可维护性
·实例分析-多层数据访问
原有的数据访问框架参考的google开源的mvp,结构如下:
改造前的具体代码如下:
改造后的代码如下:
主要变化有:
•不使用callback,改为返回Observable,可以与UI层的Rx无缝对接
•网络访问直接走Retrofit,代码更简洁
•线程管理更精细,网络操作在io线程上,运算操作在computation线程上
•主要逻辑在同一段链式代码中,提高了代码可读性
•RxJava有强大的操作符,可以进一步简化代码
上述代码虽然减少了类文件的数量,也简化了代码,但结构上还是略显繁琐,可以� 进一步简化,同时保留cache--local--net的三级数据访问结构:
使用concat+first变换操作符,按照缓存->数据库->网络的顺序依次查询数据
concat()操作符可以持有多个Observable对象,并将它们按顺序串联成队列
first()操作符可以从多个数据源中,只检索和发送第一个返回的结果,并停止后续的队列
这两个操作符组合使用,可以实现逐级访问三级缓存,并更新和保存数据的功能。
·实例分析-在MVP中使用RxAndroid
我原来的项目结构参考了google的mvp项目,大量的callback调用其实与RxAndroid的链式调用是不兼容的,但经过对mvp和RxAndroid各自特点的比较后,我还是决定把他们融合在一起使用。
原因:
•mvp可以较好的剥离和复用业务逻辑,但是付出的代价是代码量和文件数量的增加
•Rx的优势在于异步和简洁,异步不是mvp的强项,简洁正好可以解决mvp代码庞大的问题
•Rx+mvp可以各取所长,不过mvp一般是通过回调来处理异步,在代码编写上需要转换风格
•在框架上要使用mvp的分层分工思想,而在具体的逻辑实现上,使用Rx的链式代码去做数据的查询、处理、显示
在具体操作上,使用了两种修改形式:
一种部分修改是,修改Presenter中的刷新函数,从回调改为RxAndroid:
另一种修改是,放弃复杂的MVP文件结构,全部在Fragment中实现:
采用只保留Fragment的方式,看起来比起mvp方式是个倒退,但其实这反而是最合适的做法,事实上,某些业务场景下,使用mvp本来就是过度设计了,我在这个问题上的考虑如下:
•首先考虑业务情况,复用的是Fragment,而不是Presenter,那就只保留Fragment
•Presenter层的异步线程可以在Fragment中用RxLifecycle替代时,也不必须在Presenter中实现异步线程
•数据层只做网络数据访问,并做数据转换,完全可以用Retrofit替代
•生命周期全部托管至RxFragment,能减少一部分工作量并消除这部分风险
事情证明,这样改造后,文件减少、代码减少、可读性好、可维护性高
我个人对这个项目的改造持肯定态度,我相信深入学习RxAndroid后,还有进一步优化的空间
几点建议
•Rx涉及的陌生概念比较多,前期上手有一定障碍,要多动手去试
•Rx的功能非常多,这里只讲了冰山一角,建议先系统地了解一下RxJava
•要用事件订阅的思路做开发,和基于回调的思路有很多区别,要慢慢体会
•Rx的链式代码比较特殊,和其他形式的代码不能混搭,所以同一个业务模块要同时使用Rx,否则工作难以衔接
•还是因为代码形式不能混搭,旧项目要改Rx的话,也会遇到需要一口气打通整条逻辑链路的问题
•推荐再去仔细阅读本文的几篇参考文章:给 Android 开发者的 RxJava 详解;详细解析 RxAndroid 的使用方式;RxJava系列;Retrofit基本使用教程