一篇不太一样的RxJava介绍

距离上篇文章已有半年的时间,虽然这期间没什么输出,但是还是关注着RxJava和国内一些动向/文章等等,感觉很多人对RxJava还有些许误会和“错误”的理解。所以今天我们从最基础的开始,来了解一下RxJava。

我们先退回一步,忘了RxJava,讨论一个我们Android开发,甚至很多开发都会遇到的非常棘手的问题,异步问题。
举个例子,我们需要向 MVP/MVVM中的 Model层 取到一组数据,来做我们的首页展示,比如这样的:

dribbble.jpg

一个简单的Dribbble页面,由于图片很多,我们不希望在第一次请求就获得所有的图片,我们更希望先获得一些图片的MetaData,比如url等等。然后在分别异步加载,来实现一个比较好的用户体验。这里我们先不考虑RecyclerView加Glide的组合,用简单的伪代码来显示。

我们的 Model 层需要两个方法,一个是获取到我们首页图片的MetaData,一个是根据MetaData获取Bitmap。

如果万物皆可同步,那么代码非常简单:

interface Model{
    fun getList() : List<MetaData>

    fun getBitmap(metaData : MetaData) : Bitmap
}

很多同学喜欢RxJava都是因为“链式调用”看起来非常舒服,而链式调用或者说高阶函数或者操作符并不是RxJava的专利,Java8的 Stream API和Kotlin都有相关的操作,比如我们的代码在kotlin中可以这样调用

    model.getList()
        .map { model.getBitmap(it) }
        .forEach { showBitMap(it) }

是不是看起来和你们所谓的优雅简洁的RxJava链式调用一样呢?

但是同步意味着阻塞,而网络加载Bitmap大家都知道是非常耗时的。为了不阻塞用户界面(UI线程),我们希望他在后台异步执行,执行后再输出到前台。
所以我们Android中最简单直接的方法就是加入CallBack,来实现异步通信。我们的代码就变成这样

//定义CallBack
interface CallBack<T> {
    fun onSuccess(t:T)

    fun onError(error:Error)
}

interface Model{
    fun getList(callback:CallBack<List<MetaData>>)

    fun getBitmap(metaData:MetaData, callback:Callback<MetaData>)
}

看过很多RxJava教程的同学肯定觉得这里我要讲Callback Hell(回调地狱)了,然后开始展示代码RxJava来解决回调地狱的问题,但如果这样我这篇文章也没什么意义了,岂不是和很多入门文章都一样了?

我们先来看看为什么我们会出现回调地狱?而在同步的时候却可以保持我们喜欢的“链式调用”
我们在同步的时候,我们做的事情可以简化成这样:
进入主界面 -> 通过getList方法获取 List<MetaData> -> 根据list逐一操作获取bitmap -> 显示bitmap
可以看到,我们确实是一条链,所以很简单的通过stream api来实现“链式调用”

但是异步的时候呢?
进入主界面 -> getList(callback:CallBack<List<MetaData>>)方法将我们的CallBack传给后台 -> 等待后台回调我们的CallBack

重点来了,与同步的不同,我们这里不是直接获得了我们的List。而是在等待着异步的另一方通知我们。
同步的时候,我们直接拉取数据 :

1.png

而异步的时候,直观的看我们应该是在“等待”数据,异步对象向我们推送数据。

2.png

3.png

所以在我们的角度,我们是被动的,也就是英语中的reactive ,也就是所谓的响应式

我们回到我们的例子:

同步的时候,我们是这样的

interface Model{
    fun getList() : List<MetaData>

    fun getBitmap(metaData : MetaData) : Bitmap
}

而异步的时候,我们的方法没有了返回值,多了个参数,所以不能使用漂亮的“链式调用”
这是因为List 本身,就是一种同步的类型。我们每次操作List,都是对List来拉取数据。不信?我们来看下:

大家都知道List并不是最基础的集合,常用的集合还有HashMap,Set,Table,Vector等等等等。他们都有一个共同的父类: Iterable<T>

interface Iterable<out T> {
    fun iterator(): Iterator<T>
} 

这里的iterator就是迭代器,他是这个样子的

interface Iterator<out T> {

    fun next(): T
    
    fun hasNext(): Boolean
}

使用的时候也就是我们最麻烦的迭代方式:

    val i = iterator()
    while(i.hasNext()){
        val value = i.next()
    }

所以我们在Java中有了foreach,以及后面的stream api等等语法糖。
这里我们看到了,我们每次确实首先询问List,有没有值,如果有我们获取这个值,如果没有,跳出循环,对List的操作结束。读取完毕。

想象一下,如果我们有一种 AsyncList,对他的读取都是AsyncList来通知我们,然后再和同步的时候一样使用高阶函数比如map/foreach等等该多好。比如

interface Model{
    fun getList() : AsyncList<MetaData>

    fun getBitmap(metaData : MetaData) : Bitmap
}

我们就可以像同步一样,

        model.getList()
        .map { model.getBitmap(it) }
        .forEach { showBitMap(it) }

现在我们来根据Iterable设计我们的 AsyncList,上面我们知道了Iterable是同步的,是拉取数据,我们需要的AsyncList是异步的,是他推送数据给我们。
我们和List一样,给所有的异步集合来一个父类,来设计一个AsyncIterable,我们知道Iterable提供Iterator通过我们主动询问Iteratornext,hasNext等方法我们主动拉取数据。
所以我们的AsyncIterable理论上来说,应该是我们通过注册AsyncIterator的方式,将我们的AsyncIterator传递给AsyncIterable,让他来通知我们,实现异步和推送数据。
所以我们的AsyncIterable的实现应该是这样的

interface AsyncIterable<T> {
    fun iterator(iterator : AsyncIterator<T>) 
} 

(看起来好像有点眼熟?)

我们再来设计AsyncIterator,同步的方式两个方法,一个是hasNext,也就是我们主动询问iterable接下来之后还有没有值的过程,如果是异步的方式,这应该是我们的AsyncIterable,来通知我们,他接下来以后还有没有值。
所以变成了这样:

    fun hasNext(has : Boolean) 

对的,通过这种类似CallBack的方式,通知我们有没有值。true就是还有值,一旦接收到false,就代表迭代结束,我们的AsyncIterable已经遍历完成了。
另一个方法 next() 就是我们来主动询问,当前的值是什么。所以我们的AsyncIterable就是通过这个方法,来通知我们当前的值是什么,依然还是通过类似CallBack的方式:

    fun onNext(current:T)

(是不是有些眼熟?(手动滑稽))

这里有两个问题:
第一个问题:我们在这里隐藏了一个错误,因为hasNext()方法返回 false的时候不一定是没有接下来的值了,也有可能是处理当前值的时候出现了某些个错误或者异常,这样他就不能处理接下来的值,这时候我们的app就会崩溃。所以在异步的时候,我们希望我们的AsyncIterable在出错的时候,可以通知我们他出错了,我们也就不进行接下来的处理了。所以我们有了:

    fun onError(e:Throwable)

(是不是也有些眼熟?(手动滑稽))

第二个问题,在hasNext方法显然有些过于多余,因为在同步的时候,我们并不知道他究竟接下来有没有值,所以我们每次访问List的时候,要询问还有没有接下来的值,我们再进行下一步。而异步的时候,我们的AsyncIterable肯定知道他自己接下来有没有值了,我们只希望在最后他没有值的时候通知我们结束了即可,也就是说我们之前的 hasNext(true)都是多余的。我们其实只关心hasNext(true)被调用的时候。所以我们把他简化成只有最后结束的时候才调用的方法:

    fun onComplete()

这样,我们有了我们的AsyncIterator

interface AsyncIterator<T> {

    fun onNext(current:T): 
    
    fun onComplete()

    fun onError(e:Throwable)
}

对的,他就是我们RxJava中的 Observer,而我们的 Asynciterable 就对应着我们的Observable。

interface Observable<T> {
    fun subscribe(observer : Observer<T>) 
} 

由此,我给Observable下一个的定义:

Observable 是一组异步数据的集合

对的,他就是一个集合,和List,Set,Vector一样。他是一组数据,Collection可以包含0,1很多甚至无限个数据。所以Observable也可以包含0,1,n,甚至无限个数据。

当我们在处理Collection出现异常时(比如NullPointerException),我们的程序会崩溃,不会有接下来的处理。所以我们的Observable在收到onError之后,也不会再有数据推送给我们。

Collection可以通过高阶函数(High Oroder Function)进行组合,变换等等,所以作为集合之一的Observable也可以进行组合,变换。

对Iterable进行操作,我们是通过getIterator方法,来获得Iterator来进行主动询问,拉取数据实现迭代。
对Observable进行操作,我们是通过subscribe方法,来注册Observer来进行被动获取数据,由Obseravble来推送数据,实现迭代。

我们费了这么大力气,终于抽象出来一个异步的集合。那么他的好处是什么呢?

  1. 首先,这种推送数据的方式才是我们直观的,异步操作方法,我们在上文了解了,异步操作的时候,作为接收方。我们是被动的,我们没办法询问生产方到底有没有完成异步任务。只有生产方自己才知道他有没有完成生产,所以他在完成生产后通知我们,并把结果交给我们这是一种直观的解决方案。而Java或者其他高级语言没有提供这一方案。我们才自定义CallBack来实现回调。

  2. 在使用CallBack方案的时候,你知道的信息太多了。举个例子,我们上文中的

    fun getList(callback:CallBack<List<MetaData>>)

这个方法。我们通过callback知道了,这应该是一个异步操作。可能是耗时的,所以我们可能需要一个线程来执行他,执行之后,他又会给我一个List<MetaData>,而这个list却又是同步的。你需要关心的事情太多了。
俗话说,把握现在 展望未来!
我们能处理好现在的事情就已经很不错了,Observable则解决了这一问题。我们上面的方法改完之后应该是这样的

    fun getList() : Observable<List<MetaData>>

最正确的可能应该是这样的:

    fun getList() : Observable<MetaData>

对的,因为Observable本身就是个集合,无需再和同步的List嵌套使用。但是由于服务器设计原因, Observable<List<T>>这种使用方式还是很常见的。
在Observable我们无需关心这个方法究竟是怎么生成的。我们像往常一样迭代数据,我们只需要知道,他生产出数据之后,会通知我即可。
至于你到底怎么生产数据给我?在什么线程?是同步的异步的?有没有阻塞?

I don't really care!

  1. 操作符,对的因为Observable是一个数学上的集合。集合就可以进行一系列的变换,通过我们定义的高阶函数,比如map,filter等等。这些操作符不是RxJava的专利,他是我们对集合的一些常见操作。我们对List,Vector等等也应该可以进行这些操作,而Java本身没有提供这些。在Java 8后通过stream API补充了这些方法,而RxJava的一大优势就是不仅仅提供了这个异步的集合类Observable。还提供了上百个常用的操作符。

总结

通过这篇文章,我的目的是让你理解究竟什么是Observable,为什么Observable是这么设计的,好处是什么,解决了什么问题。
而答案也很明显。
Observable是一组异步数据的集合,因为异步操作和同步操作有着本质上的区别(推送数据和拉取数据)所以我们根据iterable反过来设计observable。
好处是保持了数学上的集合定义,摆脱了Callback,通过操作符(高阶函数)可以对集合实现一些变换操作。解决了通常情况异步操作不直观,复杂,回调地狱等等问题。

参考文献(部分链接可能需要梯子)

  1. A Playful Introduction to Rx by Erik Meijer
  2. Expert to Expert: Brian Beckman and Erik Meijer - Inside the .NET Reactive Framework (Rx)
  3. ReactiveX
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容