自定义RxJava之旅

自定义RxJava之旅

序幕


1.什么是RxJava?


RxJava是一个针对于Java语言的一个异步的响应式编程库。
核心是【异步】!!!

2.RxJava的特性


  1. 事件变换
  2. 链式调用
  3. 观察者模式
  4. 异常传递
  5. 线程控制

核心特性是【事件变换】!在事件变换的基础上,才有了其它的特性!

最大的用处就是像调用同步的方法那样去调用异步的方法

3.RxJava有什么用?


如果有这样的需求:获取指定标签的新闻列表,得到其中最新的新闻,保存它并返回Uri,你会怎么写?注意:getNewsList和save都是耗时操作哦!你希望可以这样:
同步方式

但是,为了避免【阻塞主线程】,你不得不写这样的迷之缩进
迷之缩进

好吧,现在确实是不会阻塞主线程了,但是,这样的代码,你不觉得难以阅读么?这还好只有两层回调,那要是来个四五层回调的嵌套,那代码,想想也是醉了,相信没有人愿意看四五层回调的代码!这就是传说中的【回调地狱】

如果用了RxJava的话,那你可以这样实现:
RxJava方式

好吧,如果你说你没玩过Lambda表达式,那好,我们就不用Lambda表达式简写了,来点原始的:

原始的RxJava方式

看,这就是RxJava的好处:简洁,能把任何复杂逻辑都能串成一条线

现在,心动了吧?那么,从现在开始,我们一步步的自己实现一个简单版的RxJava吧!相信看完之后,大家对于RxJava一定可以豁然开朗!

准备开始自定义RxJava之旅:

还是上面的例子,让大家看看如何将一个回调嵌套变成链式调用!

需求:获取指定标签的新闻列表,得到其中最新的新闻,保存它的Uri

条件:现在我们有个第三方的API,可以获取指定标签的所有新闻列表,每条新闻包含时间和内容等,同时API也可以保存单条新闻得到一个URI地址。

补充:假设第三方提供的jar里面提供了Api和ApiImpl,不可再更改:并且getNewsList耗时1.5秒,save耗时0.5秒。

第三方接口

第三方接口的实现类

自定义RxJava之旅


1.同步方式


实现代码:
同步方式实现代码

测试代码:
同步方式测试代码

输出日志:
同步方式输出日志

这里的getNewsList和save都是耗时操作,会阻塞主线程,显然同步方式不适用!

准备开始异步回调方式:

因为原始的Api并没有给我们提供异步的调用方式,所以在正式开始之前,我们需要将原始的APi转换成异步的ApiAsync,所以我们添加了几个辅助类:ApiAsync、Callback

接口的异步封装

泛型回调在ApiAsync中,我自己将api.getNewsList放在了一个子线程中,这样就不怕阻塞主线程了。这里,用到了一个自定义的线程池,不记得了的童鞋可以查看我的另一篇博文:Java-线程池

2.异步回调方式


实现代码:
异步回调方式实现代码

测试代码:
异步回调方式测试代码

输出日志:
异步回调方式输出日志

这里我没有将最后得到的Uri切换回主线程,这涉及到线程间通讯的问题,详情可查看我的另一篇博文:自定义消息传递机制

由日志可知,【主线程阻塞】的问题解决了,获取新闻列表是在thread-1执行的,保存得到URI是在thread-2执行的,但是,新的问题又来了:【回调嵌套】!怎么办?耐心看!

准备开始最原始的异步任务方式

同样的,需要先引入几个辅助类:ApiWork、AsyncWork

接口的异步任务式封装

异步任务

这是一个很重要的封装!!!
这里,我们将异步操作抽象成一个对象。
我们看接口封装类ApiWork中的异步操作的特点:

  1. 入参(tag、news),
  2. 返回AsyncWork< T>,T为(List< News>、Uri)。
  3. AsynWork内部实现了start方法,将回调传递给ApiAsync对象的(getNewsList、save)等方法。

再次说明:任何异步操作需要携带所需的常规参数和一个回调实例对象

通过将异步操作抽象成对象,以后我们就可以在异步任务的抽象类中定义一些常用的变换逻辑(RxJava称之为操作符,例如:map、flatMap等),这样,就可以避免在具体的异步任务中,要做一些变换时,又要写一大堆业务无关的模板代码!

3.异步任务方式-原始


实现代码:
异步任务方式-原始实现代码

测试代码:
异步任务方式-原始测试代码

输出日志:
异步任务方式-原始输出日志

在实现时,大家记住AsyncWork只是一个异步任务的封装,只有调用了start之后,任务才真正开始执行!姑且可以认为start也是一个特殊的操作符:

start:
作用:AsyncWork T> -> T
说明:调用start方法,执行异步任务,通过接口回调,得到结果T

这里相比第2种异步方式,似乎更复杂了啊!回调还是有两层嵌套,而且还多绕了几个弯,还不如以前好懂了呢,这不是吃饱了撑着么?别急,接下来见分晓!

准备开始异步任务方式-事件变换

根据刚才的实现流程,我们知道异步任务的变换流程是:

  1. AsyncWork>:获取新闻列表的异步任务
  2. AsyncWork:获取最新新闻的异步任务
  3. AsyncWork:保存新闻得到URI的异步任务

(其实获取AsyncWork并不是异步的操作,不过,不要曲解了我的异步任务的意思: 异步任务:是指可以支持异步操作,但并不是只支持异步操作)将一个异步任务变换成另一个异步任务,这就是【事件变换】,这也是RxJava的核心特性!!!

4.异步任务方式-事件变换


实现代码:
异步任务方式-事件变换实现代码

测试代码:
异步任务方式-事件变换测试代码

输出日志:
异步任务方式-事件变换输出日志这里,我们将三步操作,分成了三个异步任务,开始有了一点链式调用的影子了。

不过,在每个任务里面,还有着大量的业务无关的模板代码:

  1. 获取AsyncWork< News>时,只有getLatestNews(newsList)是有用的!
  2. 获取AsyncWork< Uri>时,只有apiWork.save(result)是有用的!

这显然还存在着很大的优化空间,是时候干掉那些模板代码了!

准备开始异步任务方式-操作符控制事件变换

将上一种实现方式中,获取AsyncWork< News>的逻辑泛型化,其实就是:AsyncWork< T> -> AsyncWork< R> (T指List< News>,R指News)

上面我们已经提到了:

将异步操作抽象成对象,以后我们就可以在异步任务的抽象类中定义一些常用的变换逻辑

这里的AsyncWork -> AsyncWork 显然就是一个常用的变换逻辑。于是我们拓展一下我们的AsyncWork抽象类!

定义一个map方法:
入参:T,R
出参:AsyncWork

如何实现map方法呢?先看我们上一种实现方式里面是如何实现AsyncWork> 变换成AsyncWork的:

map的来源

泛化一下就是:
map的泛化

这个castT2R的方法显然应该是抽象方法,不应该由自己实现的,于是,我们又引入了一个辅助类:

类型变换泛型接口

引用这个辅助类之后,我们的map就可以调整成:

map操作符

好了!第一个操作符map搞定:

map
作用:AsyncWork --> AsyncWork
说明:Func接口的call方法中实现(T -> R)

于是上一种实现方式的第二步-获取AsyncWork,就可以用map来简化了。
map的使用

那么上一种实现方式的第三步-获取AsyncWork< Uri>,可以使用map来简化么?
map的局限

在将News变换成Uri的时候,卡住了!!!
因为我们现有的api没有实现:News -> Uri
只有一个api(也就是save)实现了:News -> AsyncWrok< Uri>

这里我们就对map方法的使用条件也就更清楚了:存在方法:T -> R

既然map无法实现,那我们就再定义一个操作符吧!
于是我们再次拓展一下我们的AsyncWork抽象类!

定义一个flatMap方法:
入参:T,R
出参:AsyncWork< R>
条件:存在方法:T -> AsyncWork

如何实现flatMap方法呢?老规矩,先看我们上一种实现方式里面是如何实现AsyncWork 变换成AsyncWork的:

flatMap的来源

泛化一下就是:
flatMap的泛化

同理,这个castT2WorkR的方法也应该是抽象方法,不应该由自己实现的,于是,我们引入了刚才的辅助类-类型变换泛型接口,调整后得到:

flatMap的实现

5.异步任务方式-操作符控制事件变换


实现代码:
异步任务方式-操作符控制事件变换实现代码

测试代码:
异步任务方式-操作符控制事件变换测试代码

输出日志:
异步任务方式-操作符控制事件变换输出日志

现在我们再看,每个异步任务里面都没有嵌套的回调了,终于摆脱回调地狱了!

相信看了上面的代码之后,大家也都了解了map和flatMap的作用了:

想将AsyncWork< T>转成AsyncWork< R>:

  1. 如果存在方法:T -> R,则使用map
  2. 如果存在方法:T -> AsynWork< R>,则使用flatMap

但是,现在的代码显然还是没有最初的同步方式简洁,所以,是时候玩Lambda表达式!

准备开始Lambda表达式:

Lambda表达式说明:

  1. Lambda表达式可以认为是匿名方法,左边是形参,右边是方法体,一般用于接口回调的实现
  2. 特别注意:这里接口中不要有相同入参、相同出参的方法,哪怕方法名不同也不行,因为Lambda表达式是在编译时自动根据入参和出参来寻找方法的(存疑!)

一个简单的Lambda用例:
简单的Lambda用例

6.异步任务方式-Lambda简写事件变换


实现代码:
异步任务方式-Lambda简写事件变换实现代码

测试代码:
异步任务方式-Lambda简写事件变换测试代码

输出日志:
异步任务方式-Lambda简写事件变换输出日志

现在的异步方式,是不是和最开始的同步方式,看起来很相似了啊,这就是RxJava的好处,像写同步的代码一样去写异步的代码!

7.异步任务方式-事件链式变换


实现代码:
异步任务方式-事件链式变换实现代码

测试代码:
异步任务方式-事件链式变换测试代码

输出日志:
异步任务方式-事件链式变换输出日志

好了,到这里,我们自定义的RxJava就完结了。下面我们来看看真正的RxJava是怎么玩这个例子的吧!

同样的,首先定义一个辅助类:ApiRx,将原始的Api的接口转成Rx方式的接口

Rx方式的接口

辅助类搞定,下面正式开始!

8.RxJava方式-原始


实现代码:
RxJava方式-原始实现代码

测试代码:
RxJava方式-原始测试代码

输出日志:
RxJava方式-原始输出日志

9.RxJava方式-Lambda+链式


如果你偏爱链式调用,那也可以这样:
实现代码:
RxJava方式-Lambda+链式实现代码

测试代码:
RxJava方式-Lambda+链式测试代码

输出日志:
RxJava方式-Lambda+链式输出日志

总结回顾


在最后,我们回顾一下:
首先,将我们的框架AsyncWork与RxJava对比:

  1. AsyncWork = Observable 消息发射器
  1. Callback = Subscriber 消息接受器
  2. asynWork.start() = observable.subscribe()
  3. 操作符:AsyncWork只实现了map和flatMap,不过经过上面的实现,自己再另外拓展一个操作符也未必是难事!

然后,我们审视下我们的框架,具备了以下的RxJava的特性:

  1. 事件变换
  1. 链式调用
  2. 观察者模式
  3. 异常传递

就是第5项-线程控制没有实现,关于线程并发这一块,感觉自己还不是太了解,需要进一步研究,以后有成果了,再来和大家分享。

同时,我们会发现:如果一个发射器想将一个消息,发送给多个接收器,用AsyncWork是无能为力的,RxJava的话呢,也需要自己去拓展一个RxBus,感觉还是不如EventBus方便!下次有机会的话,争取再自定义一个EventBus吧,感觉这种模仿一个框架的学习方式,是最能领悟到原来框架的精髓的!

看了那么多的源码解析,但是如果没有自己去实现一次,就算知道了框架的大致结构,一些细节性的东西,还是不太明白的。

有的坑,只有自己亲自踩过,才会刻骨铭心!

所有代码均已上传到:Github,欢迎Star!

随便再给大家分享一个学习android进阶知识的好网站:有心课堂,传递给你的不仅仅是技术!

菜鸟一枚,水平有限,欢迎大家指出博文中的不足之处,小鱼将不胜感激!@qq:630709658

参考目录:


  1. NotRxJava懒人专用指南
  2. 给Android开发者的RxJava详解
  3. 深入浅出RxJava
  4. 用RxJava实现事件总线(Event Bus)
  5. 用RxJava实现事件总线RxBus并实现同类型事件的区分
  6. RxBus升级篇
2016-10-20 10:2629