RxJava 沉思錄(一):你認爲 RxJava 真的好用嗎?

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

隨後工做中有機會使用 RxJava 重構了項目的網絡請求以及緩存層,期間陸陸續續又重構了數據訪問層,以及項目中其餘的一些功能模塊,無一例外,咱們都選擇使用了 RxJava 。java

最近翻看一些技術文章,發現涉及 RxJava 的文章仍是大多以入門爲主,我嘗試從一個初學者的角度閱讀,發現不少文章都沒講到關鍵的概念點,舉的例子也不夠恰當。回想起兩年前剛剛學習 RxJava 的本身,雖然看了許多 RxJava 入門的文章,可是始終沒法理解 RxJava 究竟好在哪裏,因此必定是哪裏出問題了。因而有了這一篇反思,但願能和你一塊兒從新思考 RxJava,以及從新思考 RxJava 是否真的讓咱們的開發變得更輕鬆。git

觀察者模式有那麼神奇嗎?

幾乎全部 RxJava 入門介紹,都會用必定的篇幅去介紹 「觀察者模式」,告訴你觀察者模式是 RxJava 的核心,是基石:github

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!");
    }
})
複製代碼

年少的我不明覺厲:「好厲害,原來這是觀察者模式」,可是內心仍是感受有點不對勁:「這代碼是否是有點醜?接收到數據的回調名字竟然叫 onNext ? 」編程

可是其實觀察者並非什麼新鮮的概念,即便你是新手,你確定也已經寫過很多觀察者模式的代碼了,你能看懂下面一行代碼說明你已經對觀察者模式瞭然於胸了:promise

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
    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();
複製代碼

對比兩種寫法,能夠發現,之因此 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 存在的意義到底是什麼呢?

舉幾個常見的例子:

  1. 爲 View 設置點擊回調方法:
btn.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // callback body
    }
});
複製代碼
  1. 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);
複製代碼
  1. 使用 Retrofit 發起網絡請求:
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
    }
});
複製代碼

在平常開發中咱們時時刻刻在面對着相似的回調函數,並且容易看出來,回調函數最本質的功能就是把異步調用的結果返回給咱們,剩下的都是大同小異。因此咱們能不能不要去記憶各類各樣的回調函數,只使用一種回調呢?若是咱們定義統一的回調以下:

public class Callback<T> {
    public void onResult(T result);
}
複製代碼

那麼以上 3 種狀況,對應的回調變成了:

  1. 爲 View 設置點擊事件對應的回調爲 Callback<View>
  2. Service 組件綁定操做對應的回調爲 Callback<Pair<CompnentName, IBinder>> (onServiceConnected)、 Callback<CompnentName> (onServiceDisconnected)
  3. 使用 Retrofit 發起網絡請求對應的回調爲 Callback<List<Photo>> (onResponse)、 Callback<Throwable> (onFailure)

只要按照這種思路,咱們能夠把全部的異步回調封裝成 Callback<T> 的形式,咱們再也不須要去記憶不一樣的回調,只須要和一種回調交互就能夠了。

寫到這裏,你應該已經明白了,RxJava 存在首先最基本的意義就是 統一了全部異步任務的回調接口 。而這個接口就是 Observable<T>,這和剛剛的 Callback<T> 實際上是一個意思。此外,咱們能夠考慮讓這個回調更通用一點 —— 能夠被回調屢次,對應的,Observable 表示的就是一個事件流,它能夠發射一系列的事件(onNext),包括一個終止信號(onComplete)。

若是 RxJava 單單只是統一了回調的話,其實還並無什麼了不得的。統一回調這件事情,除了知足強迫症之外,額外的收益有限,並且須要改造已有代碼,短時間來看屬於負收益。可是 Observable 屬於 RxJava 的基礎設施,有了 Observable 之後的 RxJava 纔剛剛插上了想象力的翅膀

(未完待續)

本文屬於 "RxJava 沉思錄" 系列,歡迎閱讀本系列的其餘分享:


若是您對個人技術分享感興趣,歡迎關注個人我的公衆號:麻瓜日記,不按期更新原創技術分享,謝謝!:)

相關文章
相關標籤/搜索