RxJava 沉思錄(一):你認(rèn)為 RxJava 真的好用嗎杂抽?

本人兩年前第一次接觸 RxJava,和大多數(shù)初學(xué)者一樣韩脏,看的第一篇 RxJava 入門文章是扔物線寫的《給 Android 開發(fā)者的 RxJava 詳解》缩麸,這篇文章流傳之廣,相信幾乎所有學(xué)習(xí) RxJava 的開發(fā)者都閱讀過赡矢。盡管那篇文章定位讀者是 RxJava 入門的初學(xué)者杭朱,但是閱讀完之后還是覺得懵懵懂懂,總感覺依然不是很理解這個框架設(shè)計理念以及優(yōu)勢吹散。

隨后工作中有機會使用 RxJava 重構(gòu)了項目的網(wǎng)絡(luò)請求以及緩存層弧械,期間陸陸續(xù)續(xù)又重構(gòu)了數(shù)據(jù)訪問層,以及項目中其他的一些功能模塊空民,無一例外刃唐,我們都選擇使用了 RxJava 。

最近翻看一些技術(shù)文章界轩,發(fā)現(xiàn)涉及 RxJava 的文章還是大多以入門為主画饥,我嘗試從一個初學(xué)者的角度閱讀,發(fā)現(xiàn)很多文章都沒講到關(guān)鍵的概念點浊猾,舉的例子也不夠恰當(dāng)抖甘。回想起兩年前剛剛學(xué)習(xí) RxJava 的自己葫慎,雖然看了許多 RxJava 入門的文章衔彻,但是始終無法理解 RxJava 究竟好在哪里,所以一定是哪里出問題了偷办。于是有了這一篇反思艰额,希望能和你一起重新思考 RxJava,以及重新思考 RxJava 是否真的讓我們的開發(fā)變得更輕松爽篷。

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

幾乎所有 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!");
    }
})

年少的我不明覺厲:“好厲害铡溪,原來這是觀察者模式”,但是心里還是感覺有點不對勁:“這代碼是不是有點丑泪喊?接收到數(shù)據(jù)的回調(diào)名字居然叫 onNext ? ”

但是其實觀察者并不是什么新鮮的概念棕硫,即使你是新手,你肯定也已經(jīng)寫過不少觀察者模式的代碼了袒啼,你能看懂下面一行代碼說明你已經(jīng)對觀察者模式了然于胸了:

button.setOnClickListener(v -> doSomething());

這就是觀察者模式哈扮,OnClickListener 訂閱了 button 的點擊事件纬纪,就這么簡單。原生的寫法對比上面 RxJava 那一長串的寫法滑肉,是不是要簡單多了包各。有人可能會說,RxJava 也可以寫成一行表示:

RxView.clicks(button).subscribe(v -> doSomething());

先不說這么寫需要引入 RxBinding 這個第三方庫靶庙,不考慮這點问畅,這兩種寫法最多也只是打個平手,完全體現(xiàn)不出 RxJava 有任何優(yōu)勢六荒。

這就是我要說的第一個論點护姆,如果僅僅只是為了使用 RxJava 的觀察者模式,而把原先 Callback 的形式掏击,改為 RxJava 的 Observable 訂閱模式是沒有價值的卵皂,你只是把一種觀察者模式改寫成了另一種觀察者模式。我是實用主義者砚亭,使用 RxJava 不是為了炫技灯变,所以觀察者模式是我們使用 RxJava 的理由嗎?當(dāng)然不是捅膘。

鏈?zhǔn)骄幊毯軈柡?

鏈?zhǔn)骄幊桃彩敲看翁岬?RxJava 的時候總會出現(xiàn)的一個高頻詞匯柒凉,很多人形容鏈?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我們帶來的好處的最佳佐證篓跛。然而平心而論膝捞,我看到這個例子的時候,內(nèi)心是平靜的愧沟,并沒有像大多數(shù)文章寫得那樣蔬咬,內(nèi)心產(chǎn)生“它很長,但是很清晰”的心理活動沐寺。

首先林艘,flatMap, filter, map 這幾個操作符,對于沒有函數(shù)式編程經(jīng)驗的初學(xué)者來講混坞,并不好理解狐援。其次,雖然這段代碼用了很多 RxJava 的操作符究孕,但是其邏輯本質(zhì)并不復(fù)雜啥酱,就是在后臺線程把某個文件夾里面的以 png 結(jié)尾的圖片文件解析出來,交給 UI 線程進(jìn)行渲染厨诸。

上面這段代碼镶殷,還帶有一個反例,使用 new Thread() 的方式實現(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();

對比兩種寫法微酬,可以發(fā)現(xiàn)绘趋,之所以 RxJava 版本的縮進(jìn)減少了颤陶,是因為它利用了函數(shù)式的操作符,把原本嵌套的 for 循環(huán)邏輯展平到了同一層次陷遮,事實上滓走,我們也可以把上面那個反例的嵌套邏輯展平,既然要用 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() 有槽點以外,沒什么大毛病茬斧。RxJava 版本確實代碼更少,同時省去了一個中間變量 pngFiles梗逮,這得益于函數(shù)式編程的 API项秉,但是實際開發(fā)中,這兩種寫法無論從性能還是項目可維護性上來看慷彤,并沒有太大的差距娄蔼,甚至,如果團隊并不熟悉函數(shù)式編程底哗,后一種寫法反而更容易被大家接受岁诉。

回到剛才說的“鏈?zhǔn)骄幊獭保琑xJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函數(shù)式編程風(fēng)格帶到了帶到了低版本 Android 系統(tǒng)上跋选,確實帶給我們一些方便涕癣,但是僅此而已嗎?到目前為止我并沒有看到 RxJava 在處理事件尤其是異步事件上有什么特別的手段前标。

準(zhǔn)確的來說坠韩,我的關(guān)注點并不在大多數(shù)文章鼓吹的“鏈?zhǔn)骄幊獭边@一點上,把多個依次執(zhí)行的異步操作的調(diào)用轉(zhuǎn)化為類似同步代碼調(diào)用那樣的自上而下執(zhí)行炼列,并不是什么新鮮事只搁,而且就這個具體的例子,使用 Android 原生的 AsyncTask 或者 Handler 就可以滿足需求俭尖,RxJava 相比原生的寫法無法體現(xiàn)它的優(yōu)勢氢惋。

除此以外,對于處理異步任務(wù)稽犁,還有 Promise 這個流派焰望,使用類似這樣的 API:

promise
    .then(r1 -> task1(r1))
    .then(r2 -> task2(r2))
    .then(r3 -> task3(r3))
    ...

難道不是比 RxJava 更加簡潔直觀嗎?而且還不需要引入函數(shù)式編程的內(nèi)容已亥。這種寫法柿估,跟所謂的“邏輯簡潔”也根本沒什么關(guān)系,所以從目前看來陷猫,RxJava 在我心目只是個 “哦秫舌,還挺不錯” 的框架的妖,但是并沒有驚艷到我。

以上是我要說的第二個論點足陨,鏈?zhǔn)骄幊痰男问街皇且环N語法糖嫂粟,通過函數(shù)式的操作符可以把嵌套邏輯展平,通過別的方法也可以把嵌套邏輯展平墨缘,這只是普通操作星虹,也有其他框架可以做到相似效果。

RxJava 等于異步加簡潔嗎?

相信閱讀過本文開頭介紹的那篇 RxJava 入門文 《給 Android 開發(fā)者的 RxJava 詳解》 的開發(fā)者一定對文中兩個小標(biāo)題印象深刻:

RxJava 到底是什么镊讼? —— 一個詞:異步

RxJava 好在哪宽涌? —— 一個詞:簡潔

首先感謝扔物線,很用心地為初學(xué)者準(zhǔn)備了這篇簡潔樸實的入門文蝶棋。但是我還是想要指出卸亮,這樣的表達(dá)是不夠嚴(yán)謹(jǐn)?shù)?/strong>。

雖然我們使用 RxJava 的場景大多數(shù)與異步有關(guān)玩裙,但是這個框架并不是與異步等價的兼贸。舉個簡單的例子:

Observable.just(1,2,3).subscribe(System.out::println);

上面的代碼就是同步執(zhí)行的,和異步?jīng)]有關(guān)系吃溅。事實上溶诞,RxJava 除非你顯式切換到其他的 Scheduler,或者你使用的某些操作符隱式指定了其他 Scheduler决侈,否則 RxJava 相關(guān)代碼就是同步執(zhí)行的螺垢。

這種設(shè)計和這個框架的野心有關(guān),RxJava 是一種新的 事件驅(qū)動型 編程范式赖歌,它以異步為切入點甩苛,試圖一統(tǒng) 同步異步 的世界。
本文前面提到過:

RxJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函數(shù)式編程風(fēng)格帶到了帶到了低版本 Android 系統(tǒng)上俏站。

所以只要你愿意讯蒲,你完全可以在日常的同步編程上使用 RxJava,就好像你在使用 Java 8 的 Stream API肄扎。( 但是兩者并不等價墨林,因為 RxJava 是事件驅(qū)動型編程 )

如果你把日常的同步編程,封裝為同步事件的 Observable犯祠,那么你會發(fā)現(xiàn)旭等,同步和異步這兩種情況被 RxJava 統(tǒng)一了, 兩者具有一樣的接口,可以被無差別的對待衡载,同步和異步之間的協(xié)作也可以變得比之前更容易搔耕。

所以,到此為止,我這里的結(jié)論是:RxJava 不等于異步弃榨。

那么 RxJava 等于 簡潔 嗎菩收?我相信有一些人會說 “是的,RxJava 很簡潔”鲸睛,也有一些人會說 “不娜饵,RxJava 太糟糕了,一點都不簡潔”官辈。這兩種說法我都能理解箱舞,其實問題的本質(zhì)在于對 簡潔 這個詞的定義上。關(guān)于這個問題拳亿,后續(xù)會有一個小節(jié)專門討論晴股,但是我想提前先下一個結(jié)論,對于大多數(shù)人肺魁,RxJava 不等于簡潔电湘,有時候甚至是更難以理解的代碼以及更低的項目可維護性。

RxJava 是用來解決 Callback Hell 的嗎?

很多 RxJava 的入門文都宣揚:RxJava 是用來解決 Callback Hell (有些翻譯為“回調(diào)地獄”)問題的万搔,指的是過多的異步調(diào)用嵌套導(dǎo)致的代碼呈現(xiàn)出的難以閱讀的狀態(tài)。

我并不贊同這一點官帘。Callback Hell 這個問題瞬雹,最嚴(yán)重的重災(zāi)區(qū)是在 Web 領(lǐng)域,是使用 JavaScript 最常見的問題之一刽虹,以至于專門有一個網(wǎng)站 callbackhell.com 來討論這個問題酗捌,由于客戶端編程和 Web 前端編程具有一定的相似性,Android 編程或多或少也存在這個問題涌哲。

上面這個網(wǎng)站中胖缤,介紹了幾種規(guī)避 Callback Hell 的常見方法,無非就是把嵌套的層次移到外層空間來阀圾,不要使用匿名的回調(diào)函數(shù)哪廓,為每個回調(diào)函數(shù)命名。如果是 Java 的話初烘,對應(yīng)的涡真,避免使用匿名內(nèi)部類,為每個內(nèi)部類的對象肾筐,分配一個對象名哆料。當(dāng)然,也可以使用框架來解決這類問題吗铐,使用類似 Promise 那樣的專門為異步編程打造的框架东亦,Android 平臺上也有類似的開源版本 jdeferred

在我看來唬渗,jdeferred 那樣的框架典阵,更像是那種純粹的用來解決 Callback Hell 的框架奋渔。 至于 RxJava,前面也提到過萄喳,它是一個更有野心的框架卒稳,正確使用了 RxJava 的話,確實不會有 Callback Hell 再出現(xiàn)了他巨,但如果說 RxJava 就是用來解決 Callback Hell 的充坑,那就有點高射炮打蚊子的意味了。

如何理解 RxJava

也許閱讀了前面幾小節(jié)內(nèi)容之后染突,你的心中會和曾經(jīng)的我一樣捻爷,對 RxJava 產(chǎn)生一些消極的想法,并且會產(chǎn)生一種疑問:那么 RxJava 存在的意義究竟是什么呢份企?

舉幾個常見的例子:

  1. 為 View 設(shè)置點擊回調(diào)方法:
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 發(fā)起網(wǎng)絡(luò)請求:
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ā)中我們時時刻刻在面對著類似的回調(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 種情況囚霸,對應(yīng)的回調(diào)變成了:

  1. 為 View 設(shè)置點擊事件對應(yīng)的回調(diào)為 Callback<View>
  2. Service 組件綁定操作對應(yīng)的回調(diào)為 Callback<Pair<CompnentName, IBinder>> (onServiceConnected)、 Callback<CompnentName> (onServiceDisconnected)
  3. 使用 Retrofit 發(fā)起網(wǎng)絡(luò)請求對應(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)接口 压固。而這個接口就是 Observable<T>,這和剛剛的 Callback<T> 其實是一個意思靠闭。此外邓夕,我們可以考慮讓這個回調(diào)更通用一點 —— 可以被回調(diào)多次,對應(yīng)的阎毅,Observable 表示的就是一個事件流焚刚,它可以發(fā)射一系列的事件(onNext),包括一個終止信號(onComplete)扇调。

如果 RxJava 單單只是統(tǒng)一了回調(diào)的話矿咕,其實還并沒有什么了不起的。統(tǒng)一回調(diào)這件事情,除了滿足強迫癥以外碳柱,額外的收益有限捡絮,而且需要改造已有代碼,短期來看屬于負(fù)收益莲镣。但是 Observable 屬于 RxJava 的基礎(chǔ)設(shè)施福稳,有了 Observable 以后的 RxJava 才剛剛插上了想象力的翅膀

(未完待續(xù))

本文屬于 "RxJava 沉思錄" 系列瑞侮,歡迎閱讀本系列的其他分享:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市半火,隨后出現(xiàn)的幾起案子越妈,更是在濱河造成了極大的恐慌,老刑警劉巖钮糖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梅掠,死亡現(xiàn)場離奇詭異,居然都是意外死亡店归,警方通過查閱死者的電腦和手機阎抒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來消痛,“玉大人且叁,你說我怎么就攤上這事∫蘼” “怎么了谴古?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵质涛,是天一觀的道長稠歉。 經(jīng)常有香客問我,道長汇陆,這世上最難降的妖魔是什么怒炸? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮毡代,結(jié)果婚禮上阅羹,老公的妹妹穿的比我還像新娘。我一直安慰自己教寂,他們只是感情好捏鱼,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酪耕,像睡著了一般导梆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天看尼,我揣著相機與錄音递鹉,去河邊找鬼。 笑死藏斩,一個胖子當(dāng)著我的面吹牛躏结,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播狰域,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼媳拴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了北专?” 一聲冷哼從身側(cè)響起禀挫,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拓颓,沒想到半個月后语婴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡驶睦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年砰左,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片场航。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡缠导,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出溉痢,到底是詐尸還是另有隱情僻造,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布孩饼,位于F島的核電站髓削,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏镀娶。R本人自食惡果不足惜立膛,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梯码。 院中可真熱鬧宝泵,春花似錦、人聲如沸轩娶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳄抒。三九已至闯捎,卻和暖如春搅窿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隙券。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工男应, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人娱仔。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓沐飘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親牲迫。 傳聞我的和親對象是個殘疾皇子耐朴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容