擁抱RxJava(番外篇):關於RxJava的Tips & Tricks

W_BinaryTree發表於2017-05-02

前言:

起初寫 擁抱RxJava 系列文章。只是因為看到很多人在使用RxJava時候,並沒有很正確的理解Reactive Programming。僅僅在專案中使用了Retrofit的Rx Adapter或者使用了一點點RxBus就寫道自己的專案中用了RxJava,並以此傳道。我覺得這樣是不好的。所以寫了這一系列更好的介紹RxJava。 但是可能個人的語言能力確實略差,每次都會讓讀者有些許誤解。今天這篇我們就放鬆一下,分享一下我使用RxJava時的一些Tips & Tricks (本文在為指定背景的情況下,使用的是 RxJava 2.x 版本。 )

這篇文章很多借鑑了這個presentation中的一些技巧: Common RxJava Mistakes (視訊需要科學上網)

0 RxJava 不是網路請求庫,不是執行緒庫,RxJava是響應式程式設計在JVM上的一種實現方式!

很多人使用RxJava僅僅是因為執行緒切換方便,或者是因為Retrofit提供了這樣一個炫酷的返回方式,或者僅僅是因為RxJava這種鏈式呼叫很炫酷,又或是因為大家都用RxJava我不用怕跟不上節奏。
如果你使用RxJava僅僅是因為如上幾個原因,我建議你放棄RxJava。因為RxJava給你帶來的坑將遠多於你從RxJava中獲得的便利。曾經有一個老爺爺說過一句話:

With Great Power Comes Great Responsibility
力量越大,責任越大

RxJava能做的遠不僅僅是切換執行緒或者簡單的變換(map()filter()等等),與之相伴的就是數不盡的大坑。所以當然我這篇不可能覆蓋所有的 RxJava 的坑。如果你想使用RxJava,回頭問一下自己使用這個庫的初衷,究竟是得到得多還是失去的多?

1 observeOn vs subscribeOn

這兩個操作符可能對很多人是最常用的兩個了。 然而這中間也有很多大坑在這裡。

1.1 subscribeOn 控制上游,observeOn控制下游

很多人誤以為 subscribeOn 控制的是Observable的生成的執行緒而observeOn控制的是 subscribe() 方法發生的執行緒。 當然,這點你們可以怪扔物線大神,畢竟他在他對RxJava最著名的帖子中寫道:

  • subscribeOn(): 指定 subscribe() 所發生的執行緒,即 Observable.OnSubscribe 被啟用時所處的執行緒。或者叫做事件產生的執行緒。 * observeOn(): 指定 Subscriber 所執行在的執行緒。或者叫做事件消費的執行緒。 ——扔物線

這個觀點不能說是錯的(這點我稍後再將),但是更多的是 subscribeOn控制整個上游,而observeOn控制整個下游。 舉個例子:

Observable.just("1","2","3")
    .map(x -> x.length())
    .subscribeOn(Schedulers.io())
    .flatMap(x -> Observable.just(x,"pause"))
    .observeOn(Schedulers.computation())
    .map(x -> someHeavyCaculation(x))
    .subscribe(x -> Log.d(TAG, x));複製程式碼

這段程式碼中各個操作符是在哪些執行緒中進行的?
我們看下答案:

Observable.just("1","2","3") //IO 執行緒
    .map(x -> x.length()) //IO 執行緒
    .subscribeOn(Schedulers.io())
    .flatMap(x -> Observable.just(x,"pause")) //IO 執行緒
    .observeOn(Schedulers.computation())
    .map(x -> someHeavyCaculation(x)) //computation 執行緒
    .subscribe(x -> Log.d(TAG, x)); //computation 執行緒複製程式碼

所以我們看到了,observeOn 後面的所有操作都會在這個執行緒工作。subscribeOn 會從這個Observable生成一直到遇到其他 observeOn。所以 observeOnsubscribeOn 的位置非常關鍵。

當然,這點問題 扔物線大神在文章中也詳細的講到了:

因為 observeOn() 指定的是 Subscriber 的執行緒,而這個 Subscriber 並不是(嚴格說應該為『不一定是』,但這裡不妨理解為『不是』)subscribe() 引數中的 Subscriber ,而是 observeOn() 執行時的當前 Observable 所對應的 Subscriber ,即它的直接下級 Subscriber 。換句話說,observeOn() 指定的是它之後的操作所在的執行緒。因此如果有多次切換執行緒的需求,只要在每個想要切換執行緒的位置呼叫一次 observeOn() 即可。 ——扔物線

只不過很多人看帖子看一半,扔物線大神把這最重要的部分放到了lift()後面講,很多人看完枯燥的lift()後,選擇性忽視了最後關於 Scheduler非常關鍵的這部分。

1.2 subscribeOn只發生一次,observeOn可以使用多次

如果程式需要多次切換執行緒,使用多次observeOn是完全可以的。 而subscribeOn只有最上方的subscribeOn會起作用。這點扔物線大神的文章也補充過,大家可以回頭再重溫一下。

1.3 不是所有操作符都會在預設執行緒執行

很多操作符的預設執行執行緒並不是當前執行緒,這類操作符有一個特徵就是會提供帶有 Scheduler 引數的過載方法,比如 intervalinterval 會預設在computation執行緒執行,如果你在後面加上subscribeOn。 他還是會在computation執行緒執行,你只有在過載方法里加入其他 Scheduler,他才會在其他執行緒執行。如果你仔細看過 RxJava 的 JavaDoc。 他都會明確寫出這個操作符的預設工作執行緒。

2 如果可能,避免使用Subject 當然包括RxBus!!

subject 作為Observable的結合體,在使用時非常方便。但是在使用時,很多時候並不盡人意。

2.1 Subject 的行為是不可預期的

Subject 由於暴露 onNext 方法。非常難控制。任何有這個subject引用的物件都可以使用這個方法傳輸資料,任何訂閱了subject的人都可以接收到這個資料。
這導致你訂閱Subject後幾乎不清楚資料來源到底是誰。甚至也不知道你收到的到底是什麼資料。這也是為什麼我強烈抵制 RxBus 的一大原因。
Subject 由於自己是Observable, 他遵循Observable Contract。 如果其中某個事件出現異常,onError觸發,那麼這個Subject將再也不能使用。當然這點可以使用 Jake Wharton的 RxRelay來解決。 RxRelay就是一個沒有onComplete和onError的Subject。
所以如果你的程式中必須使用Subject, 推薦將其設為 private field並且對外只暴露他的Observable形式。

2.2 Subject 預設是Hot Observable

關於 hot/cold Observable我在這篇文章中詳細的解釋過: 擁抱RxJava(三):關於Observable的冷熱,常見的封裝方式以及誤區

Subject預設是熱的,也就是說你傳送的資訊接收者是否接受的到是不一定的。是需要根據情況分析的。具體可以看我關於hot/cold Observable 的文章。

2.3 Again。 不要在繼續使用RxBus了

RxBus 幾乎都是基於Subject的再次封裝。使得他不僅擁有了Subject是所有缺點還加入了很多缺點,比如他不是型別安全的。 我見過太多的RxBus封裝都只是一個接受Object型別的Subject。這個問題當然也有很多RxBus通過 鍵值對或ofType()等等操作解決。再比如RxBus更容易造成記憶體洩漏(因為需要將所有事件和訂閱者儲存在Subject中,)。更多歡迎再次看一下我的第一篇關於RxJava的文章: 放棄RxBus,擁抱RxJava(一)

前幾天我在Reddit上看到一個人的回覆:

I think EventBus on android is popular because people don't know how to share a java object reference between android components like a Fragment and an Activity, or 2 Activities and so on. So basically I think people don't know how 2 Activites can observe the same object for data changes which I think comes from the fact that we still don't know how to architect our apps properly.

我認為 EventBus在Android上火爆的原因是人們不知道怎麼去在Android元件,例如Activity/Fragment之間共享一個Java物件的引用。

這個回覆可以說應該是觸到了很多人的痛點。很多情況我們用EventBus僅僅是不知道如何在多個Fragment/Activity之間共享一個物件。EventBus的做法是在Bus裡登記所有的接受者。這點在RxJava裡類似,Subject/ConnectableObservable 都有類似的功能。但問題是EventBus作為一個全域性Bus,各種不同型別的事件管理會很麻煩(雖然EventBus把這些事給你做好了,RxBus要自己弄)。我們有了RxJava完全可以避免不同事件的管理。相同事件封裝成對應Observable,根據需求選擇訂閱。這樣保持了型別安全,提高了效能,邏輯更清晰。

想一想,自己使用EventBus是不是也是這個原因呢?

3 如果你還在使用RxJava 1.x 建議儘快升級2.x版本

RxJava 2.x 更新了很多新內容。比如將Backpressure機制分離出來做成Flowable等等。 而且RxJava 1.x馬上要壽終正寢,進入不再更新的模式(2017年6月)。所以還在使用RxJava 1.X 的同學們儘快更新吧。
如果你仍然處於某種原因,必須使用RxJava 1.x, 那麼也千萬不要使用Observable.create(Observable.OnSubscribe<T> f) 操作符(現已經被Deprecated)建立Observable。使用其他工廠方法或者直接升級為RxJava 2.x (現已經更新到2.1.0)才是正確的選擇。

4 關於操作符

4.1 儘量避免過多的使用操作符,能合併的操作符儘量合併。

這裡的合併不是指使用ObservableTransformer合併。而是指在邏輯上合併,比如:

    Observable.just("Hello","World","RxJava")
              .map(x -> x.length())
              .map(x -> x + 2)
              .subscribe(/**********/)複製程式碼

這裡的兩個map明顯可以寫成一個。 我們知道,每個操作符都會根據操作符的特性生成新的Observable,訂閱他的上游然後給下游傳送資料,避免使用過多的操作符可以降低記憶體抖動。
所以我不是很推薦使用ObservableTransformer來合併出來一個

ObservableTransformer applyThread = upstream ->
    upstream.subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread());複製程式碼

這樣雖然在寫法上更簡單了。但是損失了observeOn的靈活性還額外增加OverHead。得不償失。當然,如果我們使用Transformer來進行模組解耦,這當然是非常值得的。詳細可以參考我的上一篇文章:
動手做一個Full Rx App

4.2 flatMap並不保證發射順序。

flatMap是將他每個得到的item轉換成一個Observable,然後通過merge融合這些Observable。但是每個對應的Observable發射出去的一個或多個專案並不是完全有序的。如果想要保證發射順序,使用concatMap。同理,merge操作符也不保證順序,如果需要有序,使用concat

4.3 如果不是必要,不要在flatMap中使用過多的操作符。

我們剛才說了,每個item都會生成一個新的Observable,每個操作符也會。所以如果你的flatMap中有其他操作符,比如下面的程式碼:

Observable.fromIterable(list)
    .flatMap(x -> Observable.just("x",x,"y")
        .map(item -> "item" + item))
    .subscribe();複製程式碼

如果你的list中有上萬個item。 那麼你將會呼叫這個map上萬次,多生成上萬個ObservableMap來進行這個操作。 我們可以簡單的消除這個OverHead。 將map拿出來,如下:

Observable.fromIterable(list)
    .flatMap(x -> Observable.just("x",x,"y"))
    .map(item -> "item" + item)
    .subscribe();複製程式碼

這樣flatMap後的Observable會統一進行管理。省去了那上萬個ObservableMap。

4.4 如果不是必要,不要自己寫 Observable 的操作符。

Observable的每個操作符都有著很複雜的邏輯,就連很多RxJava的專家都會出錯。如果你真的想寫自己的操作符,我建議你首先閱讀這個文章:
Writing operators for 2.0
詳細的介紹瞭如何寫操作符,要遵頊哪些規則。 順便一提,這個文章有將近1700行,還有三個主要模組處於TBD階段,並沒有完全補充。寫操作符的難度可想而知。

4.5 使用ignoreElements().andThen()來進行多步操作。

很多時候我們的一套流程不僅僅包含一個操作。比如我們想通過一個List<page>來載入幾個頁面。載入後我們想進行其他操作,但又不想破壞整個鏈條結構。我們就可以通過ignoreElements().andThen()的結合:

        Observable.fromIterable(list)
                //在這裡進行載入頁面
                .doOnNext(item -> loadPage(item))
                //ignoreElements會提供給你一個Completable
                .ignoreElements()
                //andThen觸發證明上游的Completable已經結束。onComplete觸發,這是轉而進行andThen裡的操作
                .andThen(Observable.just("Complete"))
                //進行其他操作
                .subscribe(x -> System.out.println(x + "已經結束載入所有頁面"));複製程式碼

小彩蛋:RxJava和Kotlin我最近碰到的一個坑

最近使用RxJava和Kotlin。 我將

startWith(idleState());複製程式碼

寫成了

startWith{idleState()};複製程式碼

在我的IDE幾乎肉眼難以分辨的區別。由於kotlin的lambda規則,{} 把我需要的變數解析成了lambda表示式,又由於正好是單引數的lambda,可以省略引數轉換為 it 的寫法。導致我這句仍然可以編譯,但過載了錯誤的操作符。導致整個鏈條崩潰。那些年和我說kotlin語法更為安全的人你過來我給你加個BUFF!

相關文章