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

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

本人兩年前第一次接觸 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 真的

本人兩年前第一次接觸 RxJava,和大多數初學者一樣,看的第一篇 RxJava 入門文章是扔物線寫的《給 Android 開發者的 RxJava 詳解》,這篇文章流傳之廣,相信幾乎所有學習 RxJava 的開發者都閱讀過。儘管那篇文章定位讀者是 RxJava

RxJava 沈思認為 RxJava 真的

list 理念 public 圖片文件 ide 方便 復制 等於 ret 本人兩年前第一次接觸 RxJava,和大多數初學者一樣,看的第一篇 RxJava 入門文章是扔物線寫的《給 Android 開發者的 RxJava 詳解》,這篇文章流傳之廣,相信幾乎所有學習 RxJa

RxJava 沉思時間維度

轉自https://juejin.im/post/5b8f536c5188255c352d3528 在上一篇分享中,我們應該已經對 Observable 在空間維度上重新組織事件的能力 印象深刻了,那麼自然而然的,我們容易聯想到時間維度,事實上就我個人而言,我認為 Obse

在Python中用Request庫模擬登字幕庫無加密,無驗證碼

用戶名 com color 了無 1-1 value img requests log 如此簡單(不安全)的登錄表單已經不多見了。字幕庫的登錄表單如下所示,其中省去了無關緊要的內容: 1 <form class="login-form" action="/User/

數據中臺系列的企業真的需要「數據中臺」

反饋 沒有 精細化運營 畫像 保險 可能 盈利 項目 關於 如何理解數據中臺 在解決你是否需要數據中臺這個問題之前,讓我們先理理它究竟是什麽。它是工具?是方法?還是組織架構?我的回答是:都不僅僅是。數據中臺包括平臺、工具、數據、組織、流程、規範等一切與企業數據資產如何用起來

SSH原理與運用遠程登

獲得 回車 you 密碼登錄 很難 windows 註釋 設備 範圍 SSH是每一臺Linux電腦的標準配置。 隨著Linux設備從電腦逐漸擴展到手機、外設和家用電器,SSH的使用範圍也越來越廣。不僅程序員離不開它,很多普通用戶也每天使用。 SSH具備多種功能,可以用於很多

【小說連載】網絡紅顏遇到一個不收錢的出租車司機

網絡 美女 職場 生活 小說 簡介:這是一段描寫網絡工程師生活的故事。故事中沒有英雄,沒有勵誌,也沒有所謂的雞湯文化和狼性文化。有的,或許是一種對技術的執著,對愛情的渴望或者是對名利的一種追求,但又能追求到什麽呢?聲明:本故事所出現的人名,公司名均為虛構,如有雷同恰屬巧合小說將在本站博客和

.Net Core 商城微服務項目系列使用IdentityServer4構建基礎登驗證

tap .net core catch access 返回 ip) logging address fin 這裏第一次搭建,所以IdentityServer端比較簡單,後期再進行完善。 1.新建API項目MI.Service.Identity,NuGet引用Identity

nodejs web應用伺服器搭建跑起的伺服器

前言 本章會分四部分來講,在開始教程之前請準備好相關基礎知識 & 文件 JavaScript基礎;AMD概念;ES5 | ES6語法;(這些沒弄清楚估計往下看也看不下去) nodejs 環境搭建 nodejs sdk文件 express 使用方法

不知道的 Virtual DOMVirtual Dom 介紹

前言 目前最流行的兩大前端框架,React和Vue,都不約而同的藉助Virtual DOM技術提高頁面的渲染效率。那麼,什麼是Virtual DOM?它是通過什麼方式去提升頁面渲染效率的呢?本系列文章會詳細講解Virtual DOM的建立過程,並實現一個簡單的Diff演

INDEMIND帶玩轉OpenCV4.0DIS光流演算法解析

文章目錄 一.OpencCV 4.0 新特性介紹 首先是OpenCV完全支援了C++ 11 DNN(深度神經網路)模組是目前OpenCV更新最重要的模組 G-API 為演算法的硬體優化

概率論與數理統計步步推貝葉斯公式

參考資料:《概率論與數理統計》 陳希孺  2000.3/2016.8 1,概率是什麼?      概率是表示某種情況出現的可能性大小的一種數量指標,它介於0和1之間。 概

PyTorch 學習筆記讓PyTorch讀取的資料集

本文擷取自《PyTorch 模型訓練實用教程》,獲取全文pdf請點選:https://github.com/tensor-yu/PyTorch_Tutorial 文章目錄 Dataset類 構建Dataset子類 想

淺析RxJava 1.x&2.x版本使用區別及原理Observable、Flowable等基本元素原始碼解析

RxJava開源框架的風靡程度在Github上無需多言,它帶來的響應式程式設計模式和執行緒隨意切換、巢狀請求、背壓等功能給了開發者耳目一新的體驗,更是成為了大多數APP中常用的RxJava+Okhttp/Retrofit+MVP/MVVM/Clean黃金組合中的

Retrofit2+RxJava學習小計單檔案、多檔案上傳之填平的坑

從Eclipse轉戰AndroidStudio已經有兩個月了。先誇誇Google親兒子的強大吧,各種方便就不一一道來了。主要是現在的Android陣營已經不想前兩年了。各種開源框架開源庫。也正是如此,AndroidStudio匯入開源的專案非常方便。自從Goog

的月亮我的C》型別定義(typedef)

    “typedef關鍵字儘管在語法上是一種儲存型別,但正如其名所示,它用來定義新的型別名稱,而不是定義新的變數或函式”                                                                            

資料結構與演算法瞭解時間複雜度和空間複雜度到底是什麼?

1. 前言 演算法(Algorithm)是指用來操作資料、解決程式問題的一組方法。對於同一個問題,使用不同的演算法,也許最終得到的結果是一樣的,但在過程中消耗的資源和時間卻會有很大的區別。那麼我們應該如何去衡量不同演算法之間的優劣呢? 主要還是從演算法所佔用的「時間」和「空間」兩個維度去考量。 時間

JVM久識名,初居我心

聊聊JVM JVM,一個熟悉又陌生的名詞,從認識Java的第一天起,我們就會聽到這個名字,在參加工作的前一兩年,面試的時候還會經常被問到JDK,JRE,JVM這三者的區別。 JVM可以說和我們是老朋友了,但是在工作中的應用場景也許不如那些框架,但是在關鍵時候還是得靠它去搞定問題,俗話說得好,知己知彼,方能百戰

安卓JNI精細化講解,讓徹底瞭解JNI環境搭建與HelloWord

目錄 1、基礎概念 ├──1.1、JNI ├──1.2、NDK ├──1.3、CMake與ndk-build 2、環境搭建 3、Native C++ 專案(HelloWord案例) ├── 3.1、專案建立(java、kotlin) ├── 3.2、CMake的應用(詳細講解) ├── 3.3、ndk-bui

產品的定價策略想通過產品掙錢,首先產品的目標客戶得不差錢

有許多工程技術上很好,很優秀的產品,甚至一定程度上得到了認可,卻無法生存的產品,關門大吉。人們常常說是因為技術人不善於營銷,這是一方面原因,更重要的我認為是他們的產品本身,就不適合放在市場上賺錢,再怎麼營銷也很難翻盤。這無關產品本身是否優秀,而是指產品的定位和產品的形態。 ### 什麼是產品的客戶 許多技術