1. 程式人生 > >RxJava 沈思錄(一):你認為 RxJava 真的好用嗎?

RxJava 沈思錄(一):你認為 RxJava 真的好用嗎?

list 理念 public 圖片文件 ide 方便 復制 等於 ret

本人兩年前第一次接觸 RxJava,和大多數初學者一樣,看的第一篇 RxJava 入門文章是扔物線寫的《給 Android 開發者的 RxJava 詳解》,這篇文章流傳之廣,相信幾乎所有學習 RxJava 的開發者都閱讀過。盡管那篇文章定位讀者是 RxJava 入門的初學者,但是閱讀完之後還是覺得懵懵懂懂,總感覺依然不是很理解這個框架設計理念以及優勢。

隨後工作中有機會使用 RxJava 重構了項目的網絡請求以及緩存層,期間陸陸續續又重構了數據訪問層,以及項目中其他的一些功能模塊,無一例外,我們都選擇使用了 RxJava 。
最近翻看一些技術文章,發現涉及 RxJava 的文章還是大多以入門為主,我嘗試從一個初學者的角度閱讀,發現很多文章都沒講到關鍵的概念點,舉的例子也不夠恰當。回想起兩年前剛剛學習 RxJava 的自己,雖然看了許多 RxJava 入門的文章,但是始終無法理解 RxJava 究竟好在哪裏,所以一定是哪裏出問題了。於是有了這一篇反思,希望能和你一起重新思考 RxJava,以及重新思考 RxJava 是否真的讓我們的開發變得更輕松。

觀察者模式有那麽神奇嗎?
幾乎所有 RxJava 入門介紹,都會用一定的篇幅去介紹 “觀察者模式”,告訴你觀察者模式是 RxJava 的核心,是基石:
observable.subscribe(new Observer<String>() {@Override
br/>@Override
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() {@Override
br/>@Override
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
br/>@Override
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() {@Override
br/>@Override
// callback body
}
});
復制代碼
Service 組件綁定操作:

private ServiceConnection mConnection = new ServiceConnection() {@Override
br/>@Override
// callback body}
@Override
br/>}
@Override
// callback body
}
};

...
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
復制代碼
使用 Retrofit 發起網絡請求:

Call<List<Photo>> call = service.getAllPhotos();
call.enqueue(new Callback<List<Photo>>() {@Override
br/>@Override
// callback body}
@Override
br/>}
@Override
// 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 真的好用嗎?