欢迎大家加入QQ群一起讨论: 489873144(android格调小窝)
我的github地址:https://github.com/jeasonlzy
一个例子
在了解RxJava之前,我们不管什么是异步,什么是观察者模式,我们先看一个例子。
为了获得更多出现在代码中的关于公共问题的信息,我们激活了StrictMode模式。StrictMode帮助我们侦测敏感的活动,如我们无意的在主线程执行磁盘访问或者网络调用。正如你所知道的,在主线程执行繁重的或者长时的任务是不可取的。因为Android应用的主线程时UI线程,它被用来处理和UI相关的操作:这也是获得更平滑的动画体验和响应式App的唯一方法。penaltyLog(),在违规做法发生时,StrictMode将会在logcat打印一条信息。
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());
场景描述如下:
- 联网获取新闻列表
- 找到最新一条的标题
- 保存最新的标题到文件
1. 同步情况
就这么个简单的事情,我们都知道网络请求是个异步操作,这里我们先假设允许同步请求,这里看代码。
创建一个类NetApi,有两个我们需要的方法,一个联网获取列表,一个保存标题到文件的方法,暂时不予实现。
调用的地方如图
这里我们看到了方法getAndSaveLatestTitle
,其中有三行代码,分别对应场景描述中的三步,简简单单,清晰易懂。
这三行代码有个最大的特点:每一步都能返回得到一个结果,并且这个结果是下一步的参数。所以当代码的执行顺序就是我们的业务逻辑顺序。
2. 异步情况
当然,实际情况中网络请求不可能是同步阻塞的请求,我们也拿不到异步的返回值,所以这里给出异步的实现代码,对上面创建的NetApi类进行修改。
既然是异步,那么必须有回调,所以我们先定义两个接口,分别是获取新闻列表的回调和保存成功的回调。
实现上一步我们没有实现的方法,这里的网络请求我使用的是我自己的开源项目,okhttp-OkGo,详细的请看github介绍,它是封装了okhttp的标准的RESTful风格的网络框架,支持RxJava,比Retrofit更简单易用。支持大文件上传下载,上传进度回调,下载进度回调,表单上传(多文件和多参数一起上传),链式调用,可以自定义返回对象,支持Https和自签名证书,支持超时自动重连,支持cookie的持久化和自动管理,支持五种缓存模式缓存网络数据,支持301和302重定向,扩展了统一的上传管理和下载管理功能。好了,不废话,继续我们的代码。
这里的保存标题,我是简单的使用了sp来保存,其实完全没有必要开线程,这么做的唯一目的是为了体现异步处理,后面就会看到这么做的用意。
既然方法实现写完了,那么调用的地方也该改了,修改ApiHelper类如下:
我们发现,又需要再定义一个回调,用来给其他使用getAndSaveLatestTitled
的人,而代码也不像同步那样的简单明了的三步,变成了一大堆的缩进、嵌套和大括号。我们还需要再每个错误的地方手动传递错误。如果某一天业务罗建改了,其他人想要维护你的代码,想必是一件头疼的事情。那么我们能不能将代码简化一下呢?
3. 泛型回调
仔细观察上面的代码,光callback回调接口,我们就定义了三处,他们又有什么区别呢,出了onSuccess回调需要特定的数据以外,onError都需要的是异常对象,那么我们完全可以使用泛型简化回调。
创建一个新类,如下
删除上面定义的三个Callback接口,全部使用泛型Callback,
修改后的NetApi如下:
修改后的ApiHelper如下:
我们发现回调接口是少了,代码看起来好像也并没有怎么少,那么我们还能在优化么?
4. 获取返回值
我们看异步后与同步时的最大区别是什么?
同步有返回值,异步没有,异步只能嵌套
那他们的共同点是什么?
每一步都需要上一步的结果作为参数
根据这两个特点,我们猜想是不是可以定义一个临时对象,这个对象包含了上一步对它的数据进行处理的步骤,然后把这个包含了处理步骤的对象,交给下一步作为参数。这样,我最后得到的这个对象一定是包含了之前所有想对这个数据进行处理的最终所有步骤,这样,当我的异步数据真正的传递过来的时候,数据就会被之前的要处理的步骤,一步步的处理,然后给我一个最终的结果。
感觉有点绕,举个形象一点的例子,就像工厂的流水线,最开始的参数就是原料,最后的结果就是产品,那么在原料变成产品的过程就是生产线,如果这个原料只需要加工一次就能变成产品,那么在这个生产线上就只需要一台机器,对应在代码上就是一个可以执行这个加工动作的任务,如果这个产品需要很多次加工,那么在这个生产线上,就会按顺序排列着很多机器,每台机器将上一步的结果加工,变成下一台机器需要的样子,每一台机器就是一个可以执行这个加工动作的任务,那么最后我们只要得到了这个生产线和原料,我就能得到结果,至于这个生产线上的机器,那就是我提前先放(注册)好的。
还是不明白?
我们用代码说话。
我们定义这个可以执行特定任务的临时对象如下:
你没看错,上面说了一大堆,代码就是这么两行,他是什么意思呢,有了callback就开始任务,至于是什么任务,这是个抽象方法,需要具体实现。
那我们就实现一个,改写NetApi类如下:
发现写法和以前有点不一样了,区别在哪?
- 异步的时候有个参数callback,现在没有了
- 异步的时候没有返回值,现在返回了一个AsyncJob对象
问题一、那么问题来了,我们就看getNewsList方法,如果我仅仅向下面这样调用这个方法,请问网络请求会不会执行?
getNewsList(1, 20)
很明显不会,这个方法只是返回了一个包含真正去请求网络方法的对象而已,这个方法就是start()
,可是我们并没有执行,所以网络请求也不会发生。
问题二、那么返回的这个AsyncJob是个什么?
他是一个临时对象,这个对象包含了我要请求网络这一个动作,并且明确的知道,网络请求结束后,把最终的转成Response
对象回调给这个任务。那么这个任务是不是就像我预先放在生产线上的机器,他有他自己的任务,如果start方法不被调用,也就不会有请求发生,也就是这台机器也就没什么事干,只要start方法一调用,网络请求一成功,这台机器就开始按事先注册好的方法,将请求的结果解析成Response
对象。
我们继续改写ApiHelper类,如下:
调用的位置代码就长下面的样子
改写原理同之前的NetApi类。
对上面的代码解释如下:对于getAndSaveLatestTitle
方法,返回一个AsyncJob<Uri>
对象,这个对象包含了以下动作:
- 通过
getNewsList
获取含有请求网络的包装对象AsyncJob<Response>
- 执行这个包装对象的start方法。
- 一旦start方法执行,那么真正的网络请求执行
- 执行成功后回调
Callback<Response>
- 根据成功的结果找出最新一条标题
- 通过
sava
方法获取含有保存动作的对象AsyncJob<Uri>
- 执行这个包装对象的start方法。
- 最后将最终的结果通过callback传递给最外层的callback
这几行代码得自己看,最好自己写一遍,否则很难理解。
写到这里仿佛代码越写越复杂了,那么我们写了这么多的优势是什么?就一点,我们得到了包装有异步操作的返回值对象。而下一步的异步操作刚好需要上一步的返回对象,是不是感觉回到了同步调用的样子,只是代码有点难看。当然,我们还要继续简化。
5. 拆分回调
这里我们需要对getAndSaveLatestTitle
那一串难以理解的方法进行一次变化,按照同步的那三步我们再来一遍。还记得么:
- 联网获取新闻列表,得到了包含网络请求动作的临时对象
AsyncJob<Response>
- 找到最新一条的标题,根据上一步的返回值,得到新的包含了寻找最后一条新闻的临时对象
AsyncJob<String>
- 保存最新的标题到文件,根据上一步的返回值,得到一个包含了保持标题动作的临时对象
AsyncJob<Uri>
- 最后返回这个对象,这个对象就包含了上述一系列的处理流程。所以我们调用如下,如果不执行start方法,那么什么也不会发生,一旦调用,那么这一系列的动作就开始了。
这里我们已经看到了希望,最开始异步回调的时候那种嵌套逻辑被我们拆分开了,向同步一样,有返回值,可以相互组合,这才是到目前费了这么大力气的最重要的地方。
6. 映射
仔细观察第二步的代码,实际上对我们来说,只有
String title = findLatestTitle(response);
这么一行是有效代码,其余都是模板代码,所以可以把这种公共的模板代码分离出去。怎么做呢,看这个方法签名,需要接受一个类型的参数,经过一顿处理,变成另一种数据返回。而java是没法传递方法的,但是可以传递对象,所以根据这种特性,定义一个类如下,接受一个T
类型的参数,返回一个类型为R的值。
此外,我们在AsyncJob中定义一个方法,叫Map,主要作用就是转换结果,实现代码如下:
这样,第二步中的代码我们就可以简化为这样的了
这种思想看起来像模板方法设计模式,就是提供一个架子,只需要传递进来我需要的方法,就会按照以前的逻辑,将这个方法在设定好的地方执行。
同样的原理我们用在第三步上,发现类型不匹配,原因是第三步中有两个AsyncJob嵌套,我们需要把这两个AsyncJob铺平成一个,方法的泛型也从R
变成了AsyncJob<R>
,我们继续在AsyncJob中定义一个方法,叫flatMap,实现如下:
这同样,第二步中的代码我们就可以简化为这样的了
最后我们再看一遍 getAndSaveLatestTitle
方法内部的样子:
我们现在可以完全不需要这个ApiHelper类了,因为他太简单了,直接和调用处结合起来使用,加上链试调用,效果如下:
看到这些是不是感觉很惊艳,异步调用的代码具有了和同步调用一样的特性,可以任意组合,可以有统一的错误传递,没有了之前的大段的缩进和回调,逻辑清晰明了。
7. RxJava
相信看到这里已经看到了RxJava的雏形,使用AndroidStudio的代码重构功能,可以很方便的做替换
1. 将AsyncJob
改名为Observable
2. 将AsyncJob
的start
方法改名为subscribe
方法
3. 将Callback
的onSuccess
方法改名为onNext
方法,增加一个onCompleted
方法
4. 将Callback
改名为Observer
这样就完成了我们的Demo向RxJava的转换,当然这只是一个简单功能的雏形,后续我们会按照RxJava的源码风格,来改写我们的demo,让我们把RxJava的源码自己写一遍。
本文主要参照NotRxJava懒人专用指南改写,如有侵权,欢迎告知。