本人兩年前第一次接觸 RxJava,和大多數(shù)初學(xué)者一樣韩脏,看的第一篇 RxJava 入門文章是扔物線寫的《給 Android 開發(fā)者的 RxJava 詳解》缩麸,這篇文章流傳之廣,相信幾乎所有學(xué)習(xí) RxJava 的開發(fā)者都閱讀過赡矢。盡管那篇文章定位讀者是 RxJava 入門的初學(xué)者杭朱,但是閱讀完之后還是覺得懵懵懂懂,總感覺依然不是很理解這個框架設(shè)計理念以及優(yōu)勢吹散。
隨后工作中有機會使用 RxJava 重構(gòu)了項目的網(wǎng)絡(luò)請求以及緩存層弧械,期間陸陸續(xù)續(xù)又重構(gòu)了數(shù)據(jù)訪問層,以及項目中其他的一些功能模塊空民,無一例外刃唐,我們都選擇使用了 RxJava 。
最近翻看一些技術(shù)文章界轩,發(fā)現(xiàn)涉及 RxJava 的文章還是大多以入門為主画饥,我嘗試從一個初學(xué)者的角度閱讀,發(fā)現(xiàn)很多文章都沒講到關(guān)鍵的概念點浊猾,舉的例子也不夠恰當(dāng)抖甘。回想起兩年前剛剛學(xué)習(xí) RxJava 的自己葫慎,雖然看了許多 RxJava 入門的文章衔彻,但是始終無法理解 RxJava 究竟好在哪里,所以一定是哪里出問題了偷办。于是有了這一篇反思艰额,希望能和你一起重新思考 RxJava,以及重新思考 RxJava 是否真的讓我們的開發(fā)變得更輕松爽篷。
觀察者模式有那么神奇嗎?
幾乎所有 RxJava 入門介紹悴晰,都會用一定的篇幅去介紹 “觀察者模式”,告訴你觀察者模式是 RxJava 的核心逐工,是基石:
observable.subscribe(new Observer<String>() {
@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!");
}
})
年少的我不明覺厲:“好厲害铡溪,原來這是觀察者模式”,但是心里還是感覺有點不對勁:“這代碼是不是有點丑泪喊?接收到數(shù)據(jù)的回調(diào)名字居然叫 onNext
? ”
但是其實觀察者并不是什么新鮮的概念棕硫,即使你是新手,你肯定也已經(jīng)寫過不少觀察者模式的代碼了袒啼,你能看懂下面一行代碼說明你已經(jīng)對觀察者模式了然于胸了:
button.setOnClickListener(v -> doSomething());
這就是觀察者模式哈扮,OnClickListener
訂閱了 button 的點擊事件纬纪,就這么簡單。原生的寫法對比上面 RxJava 那一長串的寫法滑肉,是不是要簡單多了包各。有人可能會說,RxJava 也可以寫成一行表示:
RxView.clicks(button).subscribe(v -> doSomething());
先不說這么寫需要引入 RxBinding 這個第三方庫靶庙,不考慮這點问畅,這兩種寫法最多也只是打個平手,完全體現(xiàn)不出 RxJava 有任何優(yōu)勢六荒。
這就是我要說的第一個論點护姆,如果僅僅只是為了使用 RxJava 的觀察者模式,而把原先 Callback 的形式掏击,改為 RxJava 的 Observable
訂閱模式是沒有價值的卵皂,你只是把一種觀察者模式改寫成了另一種觀察者模式。我是實用主義者砚亭,使用 RxJava 不是為了炫技灯变,所以觀察者模式是我們使用 RxJava 的理由嗎?當(dāng)然不是捅膘。
鏈?zhǔn)骄幊毯軈柡?
鏈?zhǔn)骄幊桃彩敲看翁岬?RxJava
的時候總會出現(xiàn)的一個高頻詞匯柒凉,很多人形容鏈?zhǔn)骄幊淌?RxJava
解決異步任務(wù)的 “殺手锏”:
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) });
這段代碼出現(xiàn)的頻率非常的高,好像是 RxJava 的鏈?zhǔn)骄幊探o我們帶來的好處的最佳佐證篓跛。然而平心而論膝捞,我看到這個例子的時候,內(nèi)心是平靜的愧沟,并沒有像大多數(shù)文章寫得那樣蔬咬,內(nèi)心產(chǎn)生“它很長,但是很清晰”的心理活動沐寺。
首先林艘,flatMap
, filter
, map
這幾個操作符,對于沒有函數(shù)式編程經(jīng)驗的初學(xué)者來講混坞,并不好理解狐援。其次,雖然這段代碼用了很多 RxJava 的操作符究孕,但是其邏輯本質(zhì)并不復(fù)雜啥酱,就是在后臺線程把某個文件夾里面的以 png 結(jié)尾的圖片文件解析出來,交給 UI 線程進(jìn)行渲染厨诸。
上面這段代碼镶殷,還帶有一個反例,使用 new Thread()
的方式實現(xiàn)的版本:
new Thread() {
@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() {
@Override
public void run() {
imageCollectorView.addImage(bitmap);
}
});
}
}
}
}
}.start();
對比兩種寫法微酬,可以發(fā)現(xiàn)绘趋,之所以 RxJava 版本的縮進(jìn)減少了颤陶,是因為它利用了函數(shù)式的操作符,把原本嵌套的 for
循環(huán)邏輯展平到了同一層次陷遮,事實上滓走,我們也可以把上面那個反例的嵌套邏輯展平,既然要用 lambda
表達(dá)式帽馋,那肯定要大家都用才比較公平吧:
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
梗逮,這得益于函數(shù)式編程的 API项秉,但是實際開發(fā)中,這兩種寫法無論從性能還是項目可維護性上來看慷彤,并沒有太大的差距娄蔼,甚至,如果團隊并不熟悉函數(shù)式編程底哗,后一種寫法反而更容易被大家接受岁诉。
回到剛才說的“鏈?zhǔn)骄幊獭保琑xJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函數(shù)式編程風(fēng)格帶到了帶到了低版本 Android 系統(tǒng)上跋选,確實帶給我們一些方便涕癣,但是僅此而已嗎?到目前為止我并沒有看到 RxJava 在處理事件尤其是異步事件上有什么特別的手段前标。
準(zhǔn)確的來說坠韩,我的關(guān)注點并不在大多數(shù)文章鼓吹的“鏈?zhǔn)骄幊獭边@一點上,把多個依次執(zhí)行的異步操作的調(diào)用轉(zhuǎn)化為類似同步代碼調(diào)用那樣的自上而下執(zhí)行炼列,并不是什么新鮮事只搁,而且就這個具體的例子,使用 Android 原生的 AsyncTask
或者 Handler
就可以滿足需求俭尖,RxJava 相比原生的寫法無法體現(xiàn)它的優(yōu)勢氢惋。
除此以外,對于處理異步任務(wù)稽犁,還有 Promise
這個流派焰望,使用類似這樣的 API:
promise
.then(r1 -> task1(r1))
.then(r2 -> task2(r2))
.then(r3 -> task3(r3))
...
難道不是比 RxJava 更加簡潔直觀嗎?而且還不需要引入函數(shù)式編程的內(nèi)容已亥。這種寫法柿估,跟所謂的“邏輯簡潔”也根本沒什么關(guān)系,所以從目前看來陷猫,RxJava 在我心目只是個 “哦秫舌,還挺不錯” 的框架的妖,但是并沒有驚艷到我。
以上是我要說的第二個論點足陨,鏈?zhǔn)骄幊痰男问街皇且环N語法糖嫂粟,通過函數(shù)式的操作符可以把嵌套邏輯展平,通過別的方法也可以把嵌套邏輯展平墨缘,這只是普通操作星虹,也有其他框架可以做到相似效果。
RxJava 等于異步加簡潔嗎?
相信閱讀過本文開頭介紹的那篇 RxJava 入門文 《給 Android 開發(fā)者的 RxJava 詳解》 的開發(fā)者一定對文中兩個小標(biāo)題印象深刻:
RxJava 到底是什么镊讼? —— 一個詞:異步
RxJava 好在哪宽涌? —— 一個詞:簡潔
首先感謝扔物線,很用心地為初學(xué)者準(zhǔn)備了這篇簡潔樸實的入門文蝶棋。但是我還是想要指出卸亮,這樣的表達(dá)是不夠嚴(yán)謹(jǐn)?shù)?/strong>。
雖然我們使用 RxJava 的場景大多數(shù)與異步有關(guān)玩裙,但是這個框架并不是與異步等價的兼贸。舉個簡單的例子:
Observable.just(1,2,3).subscribe(System.out::println);
上面的代碼就是同步執(zhí)行的,和異步?jīng)]有關(guān)系吃溅。事實上溶诞,RxJava 除非你顯式切換到其他的 Scheduler
,或者你使用的某些操作符隱式指定了其他 Scheduler
决侈,否則 RxJava 相關(guān)代碼就是同步執(zhí)行的螺垢。
這種設(shè)計和這個框架的野心有關(guān),RxJava 是一種新的 事件驅(qū)動型 編程范式赖歌,它以異步為切入點甩苛,試圖一統(tǒng) 同步 和 異步 的世界。
本文前面提到過:
RxJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函數(shù)式編程風(fēng)格帶到了帶到了低版本 Android 系統(tǒng)上俏站。
所以只要你愿意讯蒲,你完全可以在日常的同步編程上使用 RxJava,就好像你在使用 Java 8 的 Stream API肄扎。( 但是兩者并不等價墨林,因為 RxJava 是事件驅(qū)動型編程 )
如果你把日常的同步編程,封裝為同步事件的 Observable
犯祠,那么你會發(fā)現(xiàn)旭等,同步和異步這兩種情況被 RxJava 統(tǒng)一了, 兩者具有一樣的接口,可以被無差別的對待衡载,同步和異步之間的協(xié)作也可以變得比之前更容易搔耕。
所以,到此為止,我這里的結(jié)論是:RxJava 不等于異步弃榨。
那么 RxJava 等于 簡潔 嗎菩收?我相信有一些人會說 “是的,RxJava 很簡潔”鲸睛,也有一些人會說 “不娜饵,RxJava 太糟糕了,一點都不簡潔”官辈。這兩種說法我都能理解箱舞,其實問題的本質(zhì)在于對 簡潔 這個詞的定義上。關(guān)于這個問題拳亿,后續(xù)會有一個小節(jié)專門討論晴股,但是我想提前先下一個結(jié)論,對于大多數(shù)人肺魁,RxJava 不等于簡潔电湘,有時候甚至是更難以理解的代碼以及更低的項目可維護性。
RxJava 是用來解決 Callback Hell 的嗎?
很多 RxJava 的入門文都宣揚:RxJava 是用來解決 Callback Hell (有些翻譯為“回調(diào)地獄”)問題的万搔,指的是過多的異步調(diào)用嵌套導(dǎo)致的代碼呈現(xiàn)出的難以閱讀的狀態(tài)。
我并不贊同這一點官帘。Callback Hell 這個問題瞬雹,最嚴(yán)重的重災(zāi)區(qū)是在 Web 領(lǐng)域,是使用 JavaScript 最常見的問題之一刽虹,以至于專門有一個網(wǎng)站 callbackhell.com 來討論這個問題酗捌,由于客戶端編程和 Web 前端編程具有一定的相似性,Android 編程或多或少也存在這個問題涌哲。
上面這個網(wǎng)站中胖缤,介紹了幾種規(guī)避 Callback Hell 的常見方法,無非就是把嵌套的層次移到外層空間來阀圾,不要使用匿名的回調(diào)函數(shù)哪廓,為每個回調(diào)函數(shù)命名。如果是 Java 的話初烘,對應(yīng)的涡真,避免使用匿名內(nèi)部類,為每個內(nèi)部類的對象肾筐,分配一個對象名哆料。當(dāng)然,也可以使用框架來解決這類問題吗铐,使用類似 Promise
那樣的專門為異步編程打造的框架东亦,Android 平臺上也有類似的開源版本 jdeferred。
在我看來唬渗,jdeferred 那樣的框架典阵,更像是那種純粹的用來解決 Callback Hell 的框架奋渔。 至于 RxJava,前面也提到過萄喳,它是一個更有野心的框架卒稳,正確使用了 RxJava 的話,確實不會有 Callback Hell 再出現(xiàn)了他巨,但如果說 RxJava 就是用來解決 Callback Hell 的充坑,那就有點高射炮打蚊子的意味了。
如何理解 RxJava
也許閱讀了前面幾小節(jié)內(nèi)容之后染突,你的心中會和曾經(jīng)的我一樣捻爷,對 RxJava 產(chǎn)生一些消極的想法,并且會產(chǎn)生一種疑問:那么 RxJava 存在的意義究竟是什么呢份企?
舉幾個常見的例子:
- 為 View 設(shè)置點擊回調(diào)方法:
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// callback body
}
});
- Service 組件綁定操作:
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
// callback body
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
// callback body
}
};
...
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
- 使用 Retrofit 發(fā)起網(wǎng)絡(luò)請求:
Call<List<Photo>> call = service.getAllPhotos();
call.enqueue(new Callback<List<Photo>>() {
@Override
public void onResponse(Call<List<Photo>> call, Response<List<Photo>> response) {
// callback body
}
@Override
public void onFailure(Call<List<Photo>> call, Throwable t) {
// callback body
}
});
在日常開發(fā)中我們時時刻刻在面對著類似的回調(diào)函數(shù)也榄,而且容易看出來,回調(diào)函數(shù)最本質(zhì)的功能就是把異步調(diào)用的結(jié)果返回給我們司志,剩下的都是大同小異甜紫。所以我們能不能不要去記憶各種各樣的回調(diào)函數(shù),只使用一種回調(diào)呢骂远?如果我們定義統(tǒng)一的回調(diào)如下:
public class Callback<T> {
public void onResult(T result);
}
那么以上 3 種情況囚霸,對應(yīng)的回調(diào)變成了:
- 為 View 設(shè)置點擊事件對應(yīng)的回調(diào)為
Callback<View>
- Service 組件綁定操作對應(yīng)的回調(diào)為
Callback<Pair<CompnentName, IBinder>>
(onServiceConnected)、Callback<CompnentName>
(onServiceDisconnected) - 使用 Retrofit 發(fā)起網(wǎng)絡(luò)請求對應(yīng)的回調(diào)為
Callback<List<Photo>>
(onResponse)激才、Callback<Throwable>
(onFailure)
只要按照這種思路拓型,我們可以把所有的異步回調(diào)封裝成 Callback<T>
的形式,我們不再需要去記憶不同的回調(diào)瘸恼,只需要和一種回調(diào)交互就可以了劣挫。
寫到這里,你應(yīng)該已經(jīng)明白了东帅,RxJava 存在首先最基本的意義就是 統(tǒng)一了所有異步任務(wù)的回調(diào)接口 压固。而這個接口就是 Observable<T>
,這和剛剛的 Callback<T>
其實是一個意思靠闭。此外邓夕,我們可以考慮讓這個回調(diào)更通用一點 —— 可以被回調(diào)多次,對應(yīng)的阎毅,Observable
表示的就是一個事件流焚刚,它可以發(fā)射一系列的事件(onNext
),包括一個終止信號(onComplete
)扇调。
如果 RxJava 單單只是統(tǒng)一了回調(diào)的話矿咕,其實還并沒有什么了不起的。統(tǒng)一回調(diào)這件事情,除了滿足強迫癥以外碳柱,額外的收益有限捡絮,而且需要改造已有代碼,短期來看屬于負(fù)收益莲镣。但是 Observable
屬于 RxJava 的基礎(chǔ)設(shè)施福稳,有了 Observable
以后的 RxJava 才剛剛插上了想象力的翅膀。
(未完待續(xù))
本文屬于 "RxJava 沉思錄" 系列瑞侮,歡迎閱讀本系列的其他分享: