本人兩年前第一次接觸 RxJava,和大多數(shù)初學(xué)者一樣,看的第一篇 RxJava 入門文章是扔物線寫的《給 Android 開發(fā)者的 RxJava 詳解》,這篇文章流傳之廣,相信幾乎所有學(xué)習(xí) RxJava 的開發(fā)者都閱讀過。盡管那篇文章定位讀者是 RxJava 入門的初學(xué)者,但是閱讀完之后還是覺得懵懵懂懂,總感覺依然不是很理解這個(gè)框架設(shè)計(jì)理念以及優(yōu)勢(shì)。
隨后工作中有機(jī)會(huì)使用 RxJava 重構(gòu)了項(xiàng)目的網(wǎng)絡(luò)請(qǐng)求以及緩存層,期間陸陸續(xù)續(xù)又重構(gòu)了數(shù)據(jù)訪問層,以及項(xiàng)目中其他的一些功能模塊,無一例外,我們都選擇使用了 RxJava 。
最近翻看一些技術(shù)文章,發(fā)現(xiàn)涉及 RxJava 的文章還是大多以入門為主,我嘗試從一個(gè)初學(xué)者的角度閱讀,發(fā)現(xiàn)很多文章都沒講到關(guān)鍵的概念點(diǎn),舉的例子也不夠恰當(dāng)?;叵肫饍赡昵皠倓倢W(xué)習(xí) RxJava 的自己,雖然看了許多 RxJava 入門的文章,但是始終無法理解 RxJava 究竟好在哪里,所以一定是哪里出問題了。于是有了這一篇反思,希望能和你一起重新思考 RxJava,以及重新思考 RxJava 是否真的讓我們的開發(fā)變得更輕松。
觀察者模式有那么神奇嗎?
幾乎所有 RxJava 入門介紹,都會(huì)用一定的篇幅去介紹 “觀察者模式”,告訴你觀察者模式是 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!");
}
})
年少的我不明覺厲:“好厲害,原來這是觀察者模式”,但是心里還是感覺有點(diǎn)不對(duì)勁:“這代碼是不是有點(diǎn)丑?接收到數(shù)據(jù)的回調(diào)名字居然叫 onNext ? ”
但是其實(shí)觀察者并不是什么新鮮的概念,即使你是新手,你肯定也已經(jīng)寫過不少觀察者模式的代碼了,你能看懂下面一行代碼說明你已經(jīng)對(duì)觀察者模式了然于胸了:
button.setOnClickListener(v -> doSomething());
這就是觀察者模式,OnClickListener 訂閱了 button 的點(diǎn)擊事件,就這么簡(jiǎn)單。原生的寫法對(duì)比上面 RxJava 那一長(zhǎng)串的寫法,是不是要簡(jiǎn)單多了。有人可能會(huì)說,RxJava 也可以寫成一行表示:
RxView.clicks(button).subscribe(v -> doSomething());
先不說這么寫需要引入 RxBinding 這個(gè)第三方庫(kù),不考慮這點(diǎn),這兩種寫法最多也只是打個(gè)平手,完全體現(xiàn)不出 RxJava 有任何優(yōu)勢(shì)。
這就是我要說的第一個(gè)論點(diǎn),如果僅僅只是為了使用 RxJava 的觀察者模式,而把原先 Callback 的形式,改為 RxJava 的 Observable 訂閱模式是沒有價(jià)值的,你只是把一種觀察者模式改寫成了另一種觀察者模式。我是實(shí)用主義者,使用 RxJava 不是為了炫技,所以觀察者模式是我們使用 RxJava 的理由嗎?當(dāng)然不是。
鏈?zhǔn)骄幊毯軈柡?
鏈?zhǔn)骄幊桃彩敲看翁岬?RxJava 的時(shí)候總會(huì)出現(xiàn)的一個(gè)高頻詞匯,很多人形容鏈?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我們帶來的好處的最佳佐證。然而平心而論,我看到這個(gè)例子的時(shí)候,內(nèi)心是平靜的,并沒有像大多數(shù)文章寫得那樣,內(nèi)心產(chǎn)生“它很長(zhǎng),但是很清晰”的心理活動(dòng)。
首先,flatMap, filter, map 這幾個(gè)操作符,對(duì)于沒有函數(shù)式編程經(jīng)驗(yàn)的初學(xué)者來講,并不好理解。其次,雖然這段代碼用了很多 RxJava 的操作符,但是其邏輯本質(zhì)并不復(fù)雜,就是在后臺(tái)線程把某個(gè)文件夾里面的以 png 結(jié)尾的圖片文件解析出來,交給 UI 線程進(jìn)行渲染。
上面這段代碼,還帶有一個(gè)反例,使用 new Thread() 的方式實(shí)現(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();
對(duì)比兩種寫法,可以發(fā)現(xiàn),之所以 RxJava 版本的縮進(jìn)減少了,是因?yàn)樗昧撕瘮?shù)式的操作符,把原本嵌套的 for 循環(huán)邏輯展平到了同一層次,事實(shí)上,我們也可以把上面那個(gè)反例的嵌套邏輯展平,既然要用 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() 有槽點(diǎn)以外,沒什么大毛病。RxJava 版本確實(shí)代碼更少,同時(shí)省去了一個(gè)中間變量 pngFiles,這得益于函數(shù)式編程的 API,但是實(shí)際開發(fā)中,這兩種寫法無論從性能還是項(xiàng)目可維護(hù)性上來看,并沒有太大的差距,甚至,如果團(tuán)隊(duì)并不熟悉函數(shù)式編程,后一種寫法反而更容易被大家接受。
回到剛才說的“鏈?zhǔn)骄幊獭保琑xJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函數(shù)式編程風(fēng)格帶到了帶到了低版本 Android 系統(tǒng)上,確實(shí)帶給我們一些方便,但是僅此而已嗎?到目前為止我并沒有看到 RxJava 在處理事件尤其是異步事件上有什么特別的手段。
準(zhǔn)確的來說,我的關(guān)注點(diǎn)并不在大多數(shù)文章鼓吹的“鏈?zhǔn)骄幊獭边@一點(diǎn)上,把多個(gè)依次執(zhí)行的異步操作的調(diào)用轉(zhuǎn)化為類似同步代碼調(diào)用那樣的自上而下執(zhí)行,并不是什么新鮮事,而且就這個(gè)具體的例子,使用 Android 原生的 AsyncTask 或者 Handler 就可以滿足需求,RxJava 相比原生的寫法無法體現(xiàn)它的優(yōu)勢(shì)。
除此以外,對(duì)于處理異步任務(wù),還有 Promise 這個(gè)流派,使用類似這樣的 API:
promise
.then(r1 -> task1(r1))
.then(r2 -> task2(r2))
.then(r3 -> task3(r3))
...
難道不是比 RxJava 更加簡(jiǎn)潔直觀嗎?而且還不需要引入函數(shù)式編程的內(nèi)容。這種寫法,跟所謂的“邏輯簡(jiǎn)潔”也根本沒什么關(guān)系,所以從目前看來,RxJava 在我心目只是個(gè) “哦,還挺不錯(cuò)” 的框架,但是并沒有驚艷到我。
以上是我要說的第二個(gè)論點(diǎn),鏈?zhǔn)骄幊痰男问街皇且环N語(yǔ)法糖,通過函數(shù)式的操作符可以把嵌套邏輯展平,通過別的方法也可以把嵌套邏輯展平,這只是普通操作,也有其他框架可以做到相似效果。
RxJava 等于異步加簡(jiǎn)潔嗎?
相信閱讀過本文開頭介紹的那篇 RxJava 入門文 《給 Android 開發(fā)者的 RxJava 詳解》 的開發(fā)者一定對(duì)文中兩個(gè)小標(biāo)題印象深刻:
RxJava 到底是什么? —— 一個(gè)詞:異步
RxJava 好在哪? —— 一個(gè)詞:簡(jiǎn)潔
首先感謝扔物線,很用心地為初學(xué)者準(zhǔn)備了這篇簡(jiǎn)潔樸實(shí)的入門文。但是我還是想要指出,這樣的表達(dá)是不夠嚴(yán)謹(jǐn)?shù)?/strong>。
雖然我們使用 RxJava 的場(chǎng)景大多數(shù)與異步有關(guān),但是這個(gè)框架并不是與異步等價(jià)的。舉個(gè)簡(jiǎn)單的例子:
Observable.just(1,2,3).subscribe(System.out::println);
上面的代碼就是同步執(zhí)行的,和異步?jīng)]有關(guān)系。事實(shí)上,RxJava 除非你顯式切換到其他的 Scheduler,或者你使用的某些操作符隱式指定了其他 Scheduler,否則 RxJava 相關(guān)代碼就是同步執(zhí)行的。
這種設(shè)計(jì)和這個(gè)框架的野心有關(guān),RxJava 是一種新的 事件驅(qū)動(dòng)型 編程范式,它以異步為切入點(diǎn),試圖一統(tǒng) 同步 和 異步 的世界。
本文前面提到過:
RxJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函數(shù)式編程風(fēng)格帶到了帶到了低版本 Android 系統(tǒng)上。
所以只要你愿意,你完全可以在日常的同步編程上使用 RxJava,就好像你在使用 Java 8 的 Stream API。( 但是兩者并不等價(jià),因?yàn)?RxJava 是事件驅(qū)動(dòng)型編程 )
如果你把日常的同步編程,封裝為同步事件的 Observable,那么你會(huì)發(fā)現(xiàn),同步和異步這兩種情況被 RxJava 統(tǒng)一了, 兩者具有一樣的接口,可以被無差別的對(duì)待,同步和異步之間的協(xié)作也可以變得比之前更容易。
所以,到此為止,我這里的結(jié)論是:RxJava 不等于異步。
那么 RxJava 等于 簡(jiǎn)潔 嗎?我相信有一些人會(huì)說 “是的,RxJava 很簡(jiǎn)潔”,也有一些人會(huì)說 “不,RxJava 太糟糕了,一點(diǎn)都不簡(jiǎn)潔”。這兩種說法我都能理解,其實(shí)問題的本質(zhì)在于對(duì) 簡(jiǎn)潔 這個(gè)詞的定義上。關(guān)于這個(gè)問題,后續(xù)會(huì)有一個(gè)小節(jié)專門討論,但是我想提前先下一個(gè)結(jié)論,對(duì)于大多數(shù)人,RxJava 不等于簡(jiǎn)潔,有時(shí)候甚至是更難以理解的代碼以及更低的項(xiàng)目可維護(hù)性。
RxJava 是用來解決 Callback Hell 的嗎?
很多 RxJava 的入門文都宣揚(yáng):RxJava 是用來解決 Callback Hell (有些翻譯為“回調(diào)地獄”)問題的,指的是過多的異步調(diào)用嵌套導(dǎo)致的代碼呈現(xiàn)出的難以閱讀的狀態(tài)。
我并不贊同這一點(diǎn)。Callback Hell 這個(gè)問題,最嚴(yán)重的重災(zāi)區(qū)是在 Web 領(lǐng)域,是使用 JavaScript 最常見的問題之一,以至于專門有一個(gè)網(wǎng)站 callbackhell.com 來討論這個(gè)問題,由于客戶端編程和 Web 前端編程具有一定的相似性,Android 編程或多或少也存在這個(gè)問題。
上面這個(gè)網(wǎng)站中,介紹了幾種規(guī)避 Callback Hell 的常見方法,無非就是把嵌套的層次移到外層空間來,不要使用匿名的回調(diào)函數(shù),為每個(gè)回調(diào)函數(shù)命名。如果是 Java 的話,對(duì)應(yīng)的,避免使用匿名內(nèi)部類,為每個(gè)內(nèi)部類的對(duì)象,分配一個(gè)對(duì)象名。當(dāng)然,也可以使用框架來解決這類問題,使用類似 Promise 那樣的專門為異步編程打造的框架,Android 平臺(tái)上也有類似的開源版本 jdeferred。
在我看來,jdeferred 那樣的框架,更像是那種純粹的用來解決 Callback Hell 的框架。 至于 RxJava,前面也提到過,它是一個(gè)更有野心的框架,正確使用了 RxJava 的話,確實(shí)不會(huì)有 Callback Hell 再出現(xiàn)了,但如果說 RxJava 就是用來解決 Callback Hell 的,那就有點(diǎn)高射炮打蚊子的意味了。
如何理解 RxJava
也許閱讀了前面幾小節(jié)內(nèi)容之后,你的心中會(huì)和曾經(jīng)的我一樣,對(duì) RxJava 產(chǎn)生一些消極的想法,并且會(huì)產(chǎn)生一種疑問:那么 RxJava 存在的意義究竟是什么呢?
舉幾個(gè)常見的例子:
- 為 View 設(shè)置點(diǎn)擊回調(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ò)請(qǐng)求:
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ā)中我們時(shí)時(shí)刻刻在面對(duì)著類似的回調(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 種情況,對(duì)應(yīng)的回調(diào)變成了:
- 為 View 設(shè)置點(diǎn)擊事件對(duì)應(yīng)的回調(diào)為
Callback<View> - Service 組件綁定操作對(duì)應(yīng)的回調(diào)為
Callback<Pair<CompnentName, IBinder>>(onServiceConnected)、Callback<CompnentName>(onServiceDisconnected) - 使用 Retrofit 發(fā)起網(wǎng)絡(luò)請(qǐng)求對(duì)應(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)接口 。而這個(gè)接口就是 Observable<T>,這和剛剛的 Callback<T> 其實(shí)是一個(gè)意思。此外,我們可以考慮讓這個(gè)回調(diào)更通用一點(diǎn) —— 可以被回調(diào)多次,對(duì)應(yīng)的,Observable 表示的就是一個(gè)事件流,它可以發(fā)射一系列的事件(onNext),包括一個(gè)終止信號(hào)(onComplete)。
如果 RxJava 單單只是統(tǒng)一了回調(diào)的話,其實(shí)還并沒有什么了不起的。統(tǒng)一回調(diào)這件事情,除了滿足強(qiáng)迫癥以外,額外的收益有限,而且需要改造已有代碼,短期來看屬于負(fù)收益。但是 Observable 屬于 RxJava 的基礎(chǔ)設(shè)施,有了 Observable 以后的 RxJava 才剛剛插上了想象力的翅膀。
(未完待續(xù))
本文屬于 "RxJava 沉思錄" 系列,歡迎閱讀本系列的其他分享: