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

prototypez發表於1970-01-01

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

隨後工作中有機會使用 RxJava 重構了專案的網路請求以及快取層,期間陸陸續續又重構了資料訪問層,以及專案中其他的一些功能模組,無一例外,我們都選擇使用了 RxJava 。

最近翻看一些技術文章,發現涉及 RxJava 的文章還是大多以入門為主,我嘗試從一個初學者的角度閱讀,發現很多文章都沒講到關鍵的概念點,舉的例子也不夠恰當。回想起兩年前剛剛學習 RxJava 的自己,雖然看了許多 RxJava 入門的文章,但是始終無法理解 RxJava 究竟好在哪裡,所以一定是哪裡出問題了。於是有了這一篇反思,希望能和你一起重新思考 RxJava,以及重新思考 RxJava 是否真的讓我們的開發變得更輕鬆。

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

幾乎所有 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!");
    }
})
複製程式碼

年少的我不明覺厲:“好厲害,原來這是觀察者模式”,但是心裡還是感覺有點不對勁:“這程式碼是不是有點醜?接收到資料的回撥名字居然叫 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
    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 沉思錄" 系列,歡迎閱讀本系列的其他分享:


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

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

相關文章