RxJava2系列之背壓策略(一)

LeiHolmes發表於2019-02-12

前言

  通過前7篇RxJava的文章,我們對RxJava1.x版本的內容進行了學習與實踐。目前RxJava已經更新到2.x了,有小夥伴問我為什麼不直接上RxJava2的教程?RxJava2是在1的基礎上進行了更新與優化,有很多相通之處,初學者的話建議還是先從RxJava1的基礎理論一步步學習。
  本系列主要通過與RxJava1比較來學習RxJava2都有哪些改變。而RxJava2中最大的優化之處就在於它解決了RxJava1中未能有效解決的背壓(Backpressure)問題。本篇我們就來看看什麼是背壓,以及RxJava1中是如何解決這個問題。
  

背壓問題

  先來個定義:背壓問題是指在RxJava觀察者模式的非同步場景中,由於被觀察者生產事件的速度遠遠快於觀察者消費事件的速度,從而導致生產的事件堆積,最後致使記憶體溢位,程式崩潰
  再上個場景:工廠生產麵包,消費者吃麵包。工廠生產麵包速度慢了,消費者處於等待狀態,來一個吃一個,這沒什麼影響。而如果工廠生產麵包的速度過快,導致消費者來不及吃,麵包就會累積的越來越多,最後就會過期浪費。
  來看程式碼演示這個問題:

//被觀察者每過1ms發射一個事件
Observable.interval(1, TimeUnit.MILLISECONDS)
        .observeOn(Schedulers.newThread())
        .subscribe((aLong) -> {
            //觀察者每過800ms處理一個事件
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.e("rx_test", "back_pressure:" + aLong);
        });複製程式碼

  以上程式碼中被觀察者發射事件的速度是觀察者處理速度的800倍,執行後就會丟擲Caused by: rx.exceptions.MissingBackpressureException的異常。背壓問題的出現需要兩個條件:

  • 觀察者與被觀察者需處於不同執行緒。
  • 被觀察者產生事件的速度需遠快與觀察者消費事件的速度。

  由於觀察者與被觀察者處於不同執行緒,所以RxJava內部使用佇列來儲存事件,Android中預設佇列快取buffersize為16,所以當事件累計超過16個時就會丟擲MissingBackpressureException的異常。解決這種問題就需要對被觀察者進行流速控制了,而背壓正是應對這種問題的一種策略

背壓策略

  背壓策略的解決思路便是響應式拉取。與RxJava觀察者模型相反,響應式拉取是觀察者主動去被觀察者那裡拉取事件,而被觀察者則是被動等待通知再發射事件。
  觀察者需要多少事件就從被觀察者那裡拉取,而不是被動接收。這樣實際上就實現了控制被觀察者的流速,達到了背壓策略的目的。
  自繪結構圖:

  再來看一下程式碼示例:

//range操作符支援背壓策略,傳送事件的速度可被控制
Observable.range(1, 10000)
        .observeOn(Schedulers.newThread())
        .subscribe(new Subscriber<Integer>() {
            @Override
            public void onStart() {
                //一定要在onStart中通知被觀察者先傳送一個事件
                request(1);
            }

            @Override
            public void onCompleted() {
                Log.e("rx_test", "reactivePull:onCompleted");
            }

            @Override
            public void onError(Throwable e) {
                Log.e("rx_test", "reactivePull:onError:" + e.getMessage());
            }

            @Override
            public void onNext(Integer n) {
                try {
                    Thread.sleep(1000);
                    Log.e("rx_test", "reactivePull:onNext:" + n);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //處理完畢之後,再通知被觀察者傳送下一個事件
                request(1);
            }
        });複製程式碼

  上述程式碼示例中,被觀察者使用了range操作符發射10000次從1開始自增的數字,在觀察者中首先於onStart()中使用request(1)向被觀察者請求了第一個事件,之後在onNext()中每延時1000ms後輸出日誌,處理完事件後再呼叫request(1)請求一個新的事件。
  輸出結果:

reactivePull:onNext:1
...1s後...
reactivePull:onNext:2
...1s後...
reactivePull:onNext:3
...1s後...
reactivePull:onNext:4
...1s後...
reactivePull:onNext:5
......複製程式碼

  由輸出結果可看出,每過1秒輸出了一個數字,是不是實現了背壓限流策略呢?需要多少事件就在觀察者中使用request(n)主動拉取。不過RxJava1.x版本中並不是所有操作符都支援request(n)的響應式拉取,例如第一個例子中的interval操作符就不支援背壓策略。而這個問題到了RxJava2.x中就得到了完美解決,且看下一篇。

其他解決方法

  RxJava1中不支援背壓策略的操作符如何解決背壓問題呢?

過濾限流

  通過使用限流操作符將被觀察者產生的大部分事件過濾拋棄掉來達到限流的目的,間接降低事件發射的速度。

  • sample:在一段時間內,只處理最後一個資料
  • throttleFirst:在一段時間內,只處理第一個資料
  • debounce:傳送一個資料,開始計時,到了規定時間內,若沒有再傳送資料,則開始處理資料,反之重新開始計時。

  這裡以sample操作符為例子:

//使用sample過濾操作符,每隔300ms取裡時間點最近的事件傳送
Observable.interval(1, TimeUnit.MILLISECONDS)
        .observeOn(Schedulers.newThread())
        .sample(300, TimeUnit.MILLISECONDS)
        .subscribe((aLong) -> Log.e("rx_test", "controlByFilter:sample:" + aLong));複製程式碼

  輸出結果:

controlByFilter:sample:280
controlByFilter:sample:577
controlByFilter:sample:878
controlByFilter:sample:1178
controlByFilter:sample:1478
controlByFilter:sample:1779
controlByFilter:sample:2078
controlByFilter:sample:2378
controlByFilter:sample:2673
......複製程式碼

  這種方式雖然實現了限流,但卻是以拋棄大部分事件為代價的,在實際場景中並不可取,大家瞭解就好。

打包快取

  在被觀察者發射事件過快,觀察者來不及處理的情況下,可以使用快取類的操作符將其中一部分打包快取起來,再一點一點的處理其中的事件。

  • buffer:將多個事件打包放入一個List中,再一起發射。
  • window:將多個事件打包放入一個Observable中,再一起發射。

  這裡以buffer操作符為例:

//使用buffer過濾操作符,將100ms內的事件打包為list傳送
Observable.interval(1, TimeUnit.MILLISECONDS)
        .observeOn(Schedulers.newThread())
        .buffer(100, TimeUnit.MILLISECONDS)
        .subscribe((longs) -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.e("rx_test", "controlByCache:buffer:" + longs.size());
        });複製程式碼

  輸出結果:

controlByCache:buffer:79
controlByCache:buffer:1001
controlByCache:buffer:1002
controlByCache:buffer:1000
controlByCache:buffer:1001
controlByCache:buffer:1001
controlByCache:buffer:1001
controlByCache:buffer:1001
controlByCache:buffer:1001
controlByCache:buffer:1000
controlByCache:buffer:1002
controlByCache:buffer:1001複製程式碼

背壓操作符

  RxJava1.x中,還有兩種效果優於以上兩種的操作符,可使不支援背壓策略的操作符支援背壓策略。

  • onBackpressureDrop:將observable傳送的事件拋棄掉,直到subscriber再次呼叫request(n)方法的時候,就傳送給它這之後的n個事件。
  • onBackpressurebuffer:把observable傳送出來的事件做快取,當request(n)方法被呼叫的時候,給下層流傳送一個item(如果給這個快取區設定了大小,那麼超過了這個大小就會丟擲異常)。

  以onBackpressureDrop為例:

Observable.interval(1, TimeUnit.MILLISECONDS)
        .onBackpressureDrop()
        .observeOn(Schedulers.newThread())
        .subscribe(new Subscriber<Long>() {
            @Override
            public void onStart() {
                super.onStart();
                Log.e("rx_test", "controlBySpecialOperator:" + "onStart");
                request(1);
            }

            @Override
            public void onCompleted() {
                Log.e("rx_test", "controlBySpecialOperator:" + "onCompleted");
            }

            @Override
            public void onError(Throwable e) {
                Log.e("rx_test", "controlBySpecialOperator:" + "onError");
            }

            @Override
            public void onNext(Long aLong) {
                Log.e("rx_test", "controlBySpecialOperator:onNext:" + aLong);
                try {
                    Thread.sleep(500);
                    request(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });複製程式碼

  輸出結果:

controlBySpecialOperator:onStart
controlBySpecialOperator:onNext:0
controlBySpecialOperator:onNext:1
controlBySpecialOperator:onNext:2
controlBySpecialOperator:onNext:3
controlBySpecialOperator:onNext:4
controlBySpecialOperator:onNext:5
controlBySpecialOperator:onNext:6
controlBySpecialOperator:onNext:7
controlBySpecialOperator:onNext:8
controlBySpecialOperator:onNext:9
controlBySpecialOperator:onNext:10
controlBySpecialOperator:onNext:11
controlBySpecialOperator:onNext:12
controlBySpecialOperator:onNext:13
controlBySpecialOperator:onNext:14
controlBySpecialOperator:onNext:15
controlBySpecialOperator:onNext:8014
controlBySpecialOperator:onNext:8015
controlBySpecialOperator:onNext:8016
controlBySpecialOperator:onNext:8017
controlBySpecialOperator:onNext:8018
controlBySpecialOperator:onNext:8019
controlBySpecialOperator:onNext:8020
controlBySpecialOperator:onNext:8021
controlBySpecialOperator:onNext:8022
......複製程式碼

  首先輸出了0-15的資料,是因為observeOn操作符內部有一個長度為16的快取區,它會首先請求16個事件快取起來再輸出。使用onBackpressureDrop可使不支援背壓的操作符也可響應觀察者的request(n)。
  注意:需呼叫.onBackpressureDrop()方法。

總結

  以上就是本篇關於RxJava1中存在的背壓問題,背壓策略的使用方法以及其他解決方法的講解。背壓在實際開發中遇到的不多,除非是大量資料,所以各位碼友瞭解一下就好。本篇旨在為引入正式的Rxjava2做一個鋪墊,敬請期待下一篇。
  技術渣一枚,有寫的不對的地方歡迎大神們留言指正,有什麼疑惑或者建議也可以在我Github上RxJavaDemo專案Issues中提出,我會及時回覆。
  附上RxJavaDemo的地址:
  RxJavaDemo
  

相關文章