RxJava 沉思录(一):你认为 RxJava 真的好用吗?

本人两年前第一次接触 RxJava,和大多数初学者一样,看的第一篇 RxJava 入门文章是扔物线写的《给 Android 开发者的 RxJava 详解》,这篇文章流传之广,相信几乎所有学习 RxJava 的开发者都阅读过。尽管那篇文章定位读者是 RxJava 入门的初学者,但是阅读完之后还是觉得懵懵懂懂,总感觉依然不是很理解这个框架设计理念以及优势。

随后工作中有机会使用 RxJava 重构了项目的网络请求以及缓存层,期间陆陆续续又重构了数据访问层,以及项目中其他的一些功能模块,无一例外,我们都选择使用了 RxJava 。
最近翻看一些技术文章,发现涉及 RxJava 的文章还是大多以入门为主,我尝试从一个初学者的角度阅读,发现很多文章都没讲到关键的概念点,举的例子也不够恰当。回想起两年前刚刚学习 RxJava 的自己,虽然看了许多 RxJava 入门的文章,但是始终无法理解 RxJava 究竟好在哪里,所以一定是哪里出问题了。于是有了这一篇反思,希望能和你一起重新思考 RxJava,以及重新思考 RxJava 是否真的让我们的开发变得更轻松。
观察者模式有那么神奇吗?
几乎所有 RxJava 入门介绍,都会用一定的篇幅去介绍 “观察者模式”,告诉你观察者模式是 RxJava 的核心,是基石:
observable.subscribe(new Observer<String>() {br/>@Override
public void onNext(String s) {
Log.d(tag, "Item: " + s);
}

@Override
public void onCompleted() {
    Log.d(tag, "Completed!");
}

@Override
public void onError(Throwable e) {
    Log.d(tag, "Error!");
}

})
复制代码年少的我不明觉厉:“好厉害,原来这是观察者模式”,但是心里还是感觉有点不对劲:“这代码是不是有点丑?接收到数据的回调名字居然叫 onNext ? ”
但是其实观察者并不是什么新鲜的概念,即使你是新手,你肯定也已经写过不少观察者模式的代码了,你能看懂下面一行代码说明你已经对观察者模式了然于胸了:
button.setOnClickListener(v -> doSomething());
复制代码这就是观察者模式,OnClickListener 订阅了 button 的点击事件,就这么简单。原生的写法对比上面 RxJava 那一长串的写法,是不是要简单多了。有人可能会说,RxJava 也可以写成一行表示:
RxView.clicks(button).subscribe(v -> doSomething());
复制代码先不说这么写需要引入 RxBinding 这个第三方库,不考虑这点,这两种写法最多也只是打个平手,完全体现不出 RxJava 有任何优势。
这就是我要说的第一个论点,如果仅仅只是为了使用 RxJava 的观察者模式,而把原先 Callback 的形式,改为 RxJava 的 Observable 订阅模式是没有价值的,你只是把一种观察者模式改写成了另一种观察者模式。我是实用主义者,使用 RxJava 不是为了炫技,所以观察者模式是我们使用 RxJava 的理由吗?当然不是。
链式编程很厉害吗?
链式编程也是每次提到 RxJava 的时候总会出现的一个高频词汇,很多人形容链式编程是 RxJava 解决异步任务的 “杀手锏”:
Observable.from(folders)
.flatMap((Func1) (folder) -> { Observable.from(file.listFiles()) })
.filter((Func1) (file) -> { file.getName().endsWith(".png") })
.map((Func1) (file) -> { getBitmapFromFile(file) })
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((Action1) (bitmap) -> { imageCollectorView.addImage(bitmap) });
复制代码这段代码出现的频率非常的高,好像是 RxJava 的链式编程给我们带来的好处的最佳佐证。然而平心而论,我看到这个例子的时候,内心是平静的,并没有像大多数文章写得那样,内心产生“它很长,但是很清晰”的心理活动。
首先,flatMap, filter, map 这几个操作符,对于没有函数式编程经验的初学者来讲,并不好理解。其次,虽然这段代码用了很多 RxJava 的操作符,但是其逻辑本质并不复杂,就是在后台线程把某个文件夹里面的以 png 结尾的图片文件解析出来,交给 UI 线程进行渲染。
上面这段代码,还带有一个反例,使用 new Thread() 的方式实现的版本:
new Thread() {br/>@Override
public void run() {
super.run();
for (File folder : folders) {
File[] files = folder.listFiles();
for (File file : files) {
if (file.getName().endsWith(".png")) {
final Bitmap bitmap = getBitmapFromFile(file);
getActivity().runOnUiThread(new Runnable() {
br/>@Override
public void run() {
imageCollectorView.addImage(bitmap);
}
});
}
}
}
}
}.start();
复制代码对比两种写法,可以发现,之所以 RxJava 版本的缩进减少了,是因为它利用了函数式的操作符,把原本嵌套的 for 循环逻辑展平到了同一层次,事实上,我们也可以把上面那个反例的嵌套逻辑展平,既然要用 lambda 表达式,那肯定要大家都用才比较公平吧:
new Thread(() -> {
File[] pngFiles = new File[]{};
for (File folder : folders) {
pngFiles = ArrayUtils.addAll(pngFiles, folder.listFiles());
}
for (File file : pngFiles) {
if (file.getName().endsWith(".png")) {
final Bitmap bitmap = getBitmapFromFile(file);
getActivity().runOnUiThread(() -> imageCollectorView.addImage(bitmap));
}
}
}).start();
复制代码坦率地讲,这段代码除了 new Thread().start() 有槽点以外,没什么大毛病。RxJava 版本确实代码更少,同时省去了一个中间变量 pngFiles,这得益于函数式编程的 API,但是实际开发中,这两种写法无论从性能还是项目可维护性上来看,并没有太大的差距,甚至,如果团队并不熟悉函数式编程,后一种写法反而更容易被大家接受。
回到刚才说的“链式编程”,RxJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函数式编程风格带到了带到了低版本 Android 系统上,确实带给我们一些方便,但是仅此而已吗?到目前为止我并没有看到 RxJava 在处理事件尤其是异步事件上有什么特别的手段。
准确的来说,我的关注点并不在大多数文章鼓吹的“链式编程”这一点上,把多个依次执行的异步操作的调用转化为类似同步代码调用那样的自上而下执行,并不是什么新鲜事,而且就这个具体的例子,使用 Android 原生的 AsyncTask 或者 Handler 就可以满足需求,RxJava 相比原生的写法无法体现它的优势。
除此以外,对于处理异步任务,还有 Promise 这个流派,使用类似这样的 API:
promise
.then(r1 -> task1(r1))
.then(r2 -> task2(r2))
.then(r3 -> task3(r3))
...
复制代码难道不是比 RxJava 更加简洁直观吗?而且还不需要引入函数式编程的内容。这种写法,跟所谓的“逻辑简洁”也根本没什么关系,所以从目前看来,RxJava 在我心目只是个 “哦,还挺不错” 的框架,但是并没有惊艳到我。
以上是我要说的第二个论点,链式编程的形式只是一种语法糖,通过函数式的操作符可以把嵌套逻辑展平,通过别的方法也可以把嵌套逻辑展平,这只是普通操作,也有其他框架可以做到相似效果。
RxJava 等于异步加简洁吗?
相信阅读过本文开头介绍的那篇 RxJava 入门文 《给 Android 开发者的 RxJava 详解》 的开发者一定对文中两个小标题印象深刻:

RxJava 到底是什么? —— 一个词:异步

RxJava 好在哪? —— 一个词:简洁

首先感谢扔物线,很用心地为初学者准备了这篇简洁朴实的入门文。但是我还是想要指出,这样的表达是不够严谨的。
虽然我们使用 RxJava 的场景大多数与异步有关,但是这个框架并不是与异步等价的。举个简单的例子:
Observable.just(1,2,3).subscribe(System.out::println);
复制代码上面的代码就是同步执行的,和异步没有关系。事实上,RxJava 除非你显式切换到其他的 Scheduler,或者你使用的某些操作符隐式指定了其他 Scheduler,否则 RxJava 相关代码就是同步执行的。
这种设计和这个框架的野心有关,RxJava 是一种新的 事件驱动型 编程范式,它以异步为切入点,试图一统 同步 和 异步 的世界。
本文前面提到过:

RxJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函数式编程风格带到了带到了低版本 Android 系统上。

所以只要你愿意,你完全可以在日常的同步编程上使用 RxJava,就好像你在使用 Java 8 的 Stream API。( 但是两者并不等价,因为 RxJava 是事件驱动型编程 )
如果你把日常的同步编程,封装为同步事件的 Observable,那么你会发现,同步和异步这两种情况被 RxJava 统一了, 两者具有一样的接口,可以被无差别的对待,同步和异步之间的协作也可以变得比之前更容易。
所以,到此为止,我这里的结论是:RxJava 不等于异步。
那么 RxJava 等于 简洁 吗?我相信有一些人会说 “是的,RxJava 很简洁”,也有一些人会说 “不,RxJava 太糟糕了,一点都不简洁”。这两种说法我都能理解,其实问题的本质在于对 简洁 这个词的定义上。关于这个问题,后续会有一个小节专门讨论,但是我想提前先下一个结论,对于大多数人,RxJava 不等于简洁,有时候甚至是更难以理解的代码以及更低的项目可维护性。
RxJava 是用来解决 Callback Hell 的吗?
很多 RxJava 的入门文都宣扬:RxJava 是用来解决 Callback Hell (有些翻译为“回调地狱”)问题的,指的是过多的异步调用嵌套导致的代码呈现出的难以阅读的状态。
我并不赞同这一点。Callback Hell 这个问题,最严重的重灾区是在 Web 领域,是使用 JavaScript 最常见的问题之一,以至于专门有一个网站 callbackhell.com 来讨论这个问题,由于客户端编程和 Web 前端编程具有一定的相似性,Android 编程或多或少也存在这个问题。
上面这个网站中,介绍了几种规避 Callback Hell 的常见方法,无非就是把嵌套的层次移到外层空间来,不要使用匿名的回调函数,为每个回调函数命名。如果是 Java 的话,对应的,避免使用匿名内部类,为每个内部类的对象,分配一个对象名。当然,也可以使用框架来解决这类问题,使用类似 Promise 那样的专门为异步编程打造的框架,Android 平台上也有类似的开源版本 jdeferred。
在我看来,jdeferred 那样的框架,更像是那种纯粹的用来解决 Callback Hell 的框架。 至于 RxJava,前面也提到过,它是一个更有野心的框架,正确使用了 RxJava 的话,确实不会有 Callback Hell 再出现了,但如果说 RxJava 就是用来解决 Callback Hell 的,那就有点高射炮打蚊子的意味了。
如何理解 RxJava
也许阅读了前面几小节内容之后,你的心中会和曾经的我一样,对 RxJava 产生一些消极的想法,并且会产生一种疑问:那么 RxJava 存在的意义究竟是什么呢?
举几个常见的例子:

为 View 设置点击回调方法:

btn.setOnClickListener(new OnClickListener() {br/>@Override
public void onClick(View v) {
// callback body
}
});
复制代码
Service 组件绑定操作:

private ServiceConnection mConnection = new ServiceConnection() {br/>@Override
public void onServiceConnected(ComponentName className, IBinder service) {
// callback body
br/>}
@Override
public void onServiceDisconnected(ComponentName arg0) {
// callback body
}
};

...
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
复制代码
使用 Retrofit 发起网络请求:

Call<List<Photo>> call = service.getAllPhotos();
call.enqueue(new Callback<List<Photo>>() {
br/>@Override
public void onResponse(Call<List<Photo>> call, Response<List<Photo>> response) {
// callback body
br/>}
@Override
public void onFailure(Call<List<Photo>> call, Throwable t) {
// callback body
}
});
复制代码在日常开发中我们时时刻刻在面对着类似的回调函数,而且容易看出来,回调函数最本质的功能就是把异步调用的结果返回给我们,剩下的都是大同小异。所以我们能不能不要去记忆各种各样的回调函数,只使用一种回调呢?如果我们定义统一的回调如下:
public class Callback<T> {
public void onResult(T result);
}
复制代码那么以上 3 种情况,对应的回调变成了:

为 View 设置点击事件对应的回调为 Callback<View>
Service 组件绑定操作对应的回调为 Callback<Pair<CompnentName, IBinder>> (onServiceConnected)、 Callback<CompnentName> (onServiceDisconnected)
使用 Retrofit 发起网络请求对应的回调为 Callback<List<Photo>> (onResponse)、 Callback<Throwable> (onFailure)

只要按照这种思路,我们可以把所有的异步回调封装成 Callback<T> 的形式,我们不再需要去记忆不同的回调,只需要和一种回调交互就可以了。
写到这里,你应该已经明白了,RxJava 存在首先最基本的意义就是 统一了所有异步任务的回调接口 。而这个接口就是 Observable<T>,这和刚刚的 Callback<T> 其实是一个意思。此外,我们可以考虑让这个回调更通用一点 —— 可以被回调多次,对应的,Observable 表示的就是一个事件流,它可以发射一系列的事件(onNext),包括一个终止信号(onComplete)。
如果 RxJava 单单只是统一了回调的话,其实还并没有什么了不起的。统一回调这件事情,除了满足强迫症以外,额外的收益有限,而且需要改造已有代码,短期来看属于负收益。但是 Observable 属于 RxJava 的基础设施,有了 Observable 以后的 RxJava 才刚刚插上了想象力的翅膀。

RxJava 沉思录(一):你认为 RxJava 真的好用吗?

原文地址:http://blog.51cto.com/13917525/2171233

时间: 2024-10-11 16:00:33

RxJava 沉思录(一):你认为 RxJava 真的好用吗?的相关文章

Atitit。 沉思录 与it软件开发管理中的总结 读后感

Atitit. 沉思录 与it软件开发管理中的总结 读后感 1. <沉思录>,古罗马唯一一位哲学家皇帝马可·奥勒留所著 2 2. 沉思录与it软件开发管理中的总结 2 2.1. 要有自己的培训..(不要总是依靠公共图书馆) 2 2.2. 要做大架构,优先大架构 2 2.3. 各司其职 世间万物各有所用,各司其职 2 2.4. 优秀的培训不一定能造就出强大的成员...但总比没有强 2 2.5. 顺势而为,随遇而安. 2 2.6. 看穿生死,淡泊名利. 2 2.7. 保持理智,洞察世事 2 2.8

读书笔记——《沉思录》(2/4)

经典摘抄与感悟 卷六 2.如果你在履行你的职责,那么不管你是冻馁还是饱暖.嗜睡还是振作,被人指责还是被人赞扬,垂死还是做别的什么事情,让它们对你都毫无差别.因为这是生活中的活动之一,我们赴死要经过这一活动,那么在这一活动中做好我们手头要做的事就足够了. 笔记:第6卷只要你是在做你应该的事情,你就不应该被外物所烦扰心情.这给我的启示是:无论生活中你经历了什么,或者无论你被生活裹携不得不做什么,都不要觉得这是多么不幸不公的事,要把自己经历的一切快乐和悲伤看做是平常事,一切都是没什么的大不了的.只要自

《C++沉思录》:类设计者的核查表——有关class的11问

本文的11个问题提取自<C++沉思录>第四章.所有问题的说明均为自己补充. 1 你的类需要一个构造函数吗? --正确的定义构造函数,把握好构造函数的职能范围 有些类太简单,它们的结构就是它们的接口,所以不需要构造函数. class print{ void print1(){cout<<"1"<<endl;} void print2(){cout<<"2"<<endl;} void print3(){cout

设计模式沉思录——互动出版网

这篇是计算机类的优质预售推荐>>>><设计模式沉思录> GoF成员.<设计模式>一书作者之一John M. Vlissides为你揭开模式设计的神秘面纱 内容简介 本书在GoF的<设计模式>一书的基础上进行了拓展,运用其中的概念,介绍了一些技巧,帮助读者决定在不同的情况下应该使用哪些模式,以及不应该使用哪些模式.本书不仅对已有的一些模式提出新的见解,还让读者见证开发新模式的整个过程. 本书适合使用设计模式的软件开发人员阅读. 作译者 作者介绍 J

马可奥勒留《沉思录》读后感作文6000字

马可奥勒留<沉思录>读后感作文6000字:马可奥勒留的<沉思录>早有耳闻,一直没有去读是因为还没有碰到靠自己的之前的思考模式解决不了的难题.但近一年多来的迷茫让我认识到在执行层面已经无法找到问题的答案,我需要它.这本书的阅读体验也是非常有意思,最让我觉得震惊的是,他那循循善诱的表述方式以及其哲学观真的和四水平时和我讲那些碎片化的见解几乎一模一样.我一边看一边有种四水在我耳边轻声细语的感受,四水说,他的那些想法只是出于他最最朴素的自然观.这让我觉得他们之间仿佛有一种穿越了两千年的惺惺

C++沉思录之二——虚函数使用的时机

虚函数使用的时机 为什么虚函数不总是适用? 1. 虚函数有事会带来很大的消耗: 2. 虚函数不总是提供所需的行为: 3. 当我们不考虑继承当前类时,不必使用虚函数. 必须使用虚函数的情况: 1. 当你想删除一个表面上指向基类对象,实际却是指向派生类对象的指针,就需要虚析构函数. C++沉思录之二--虚函数使用的时机,布布扣,bubuko.com

个性化的亲切——《沉思录》引发的感悟

记得初中那阵子,曾经追过明星,甚至美的标准也变成了他——恨不得所有的明星都是长的和他一样,唱的和他一样.除了他的歌,我几乎欣赏不了其他人的歌. 还记得差不多在那个年纪,曾经幻想过世界“大统”——我认为“大统”是达到“大同”,消弭纷争的有效方式.惭愧,后来知道希特勒也是这么想的.此乃后话,不提. 也几乎是那个时候,我不愿再做“出头鸟”,我相信“人多力量大”,我总愿意融在身边的“圈子”,不想显得自己不合群. 个性化,在我们的应试教育体制中从来都没有得到特别的提倡,如果没有良师益友的及时提点,一定会让

迷你MVVM框架 avalonjs 沉思录 第1节 土耳其开局

#cnblogs_post_body p{ text-indent:2em; margin-top: 1em; } 正如一切传说的开端那样,有一远古巨神开天辟地,然后就是其他半神喧宾夺主.我们对最巨贡献与创建力的远古巨神懵懂不知,却对巫师们的话语津津乐道.这同样也是我们前端的现实. MVVM是来自.NET,另一个遥远的界域.前端,相对于后端,怎么看都是蛮夷之地.JS这个肩负着前端一切交互工作的语言,竟然被视为恶魔,屡屡被屏蔽禁用.些微可用的脚本,变量与函数没有组织地野蛮生长着,直到JAVA的传教

Trie树沉思录(1)

发现自己已经很久没有写解题报告了,很大一部分是因为懒,做完题之后不想再怎样了~不过最近发现写解题报告确实是有好处的,一方面可以复习,一方面可以梳理.还有就是可以给自己的岁月留下一点什么东西~今天是五一劳动节,就应该要劳动!我要重新着手写我的博客了~ 最近几个星期都在研究字符串,有点难,不过到现在为止Trie数学得还算是有那么点意思,写篇博文来记录一下! (关于Trie数是什么东西我就不想写了,我只写我个人的一些思考) Trie树通过共享前缀来达到了节约内存的目标,十分的强大!关于他的实现大概有两