關於RxJava可能被忽略的X個知識點

一 操作符之痛

在RxJava中,我們使用最爽的莫過於對一個事件流使用各種操作符,使其按照我們的指令執行各種操作,讀過源碼的同學對lift比較熟悉,它是大部分操作符實現的基礎,但是一個lift操作可能會產生多個多餘的對象。我們以最簡單的Map爲例,由於使用了lift操作,我們至少生成了三個多餘的類:Observable OnSubscribe 和 一個OperatorMap,在某些特殊的場景下面,比如 flatMap操作符,如果我們在內層Observable中使用了各種操作符,那麼會產生很多短生命週期的對象,這意味着更多的內存垃圾和GC抖動。

所以我們在使用操作符的時候,需要留意在實現相同轉換效果的情況下,是否可以對操作符組合做一定的優化。

Observable... .map().filter()     VS   Observable... .filter().map()

顯然後面的表達方式更高效,因爲被過濾掉的元素就不需要進行map操作了。

 Observable...flatMap(return observable.map();)   VS Observable...flatMap(return observable;).map()

第一種表達方式中相當於對內部創建的Observable做一些轉換操作,我們知道大部分轉換操作都會重新生成新的Observable,所以在內部Observable中使用各種轉換會產生較多的中間對象。

 Observable...flatMap(returnobservable.subscribeOn(Schedulers.io());)
                  .subscribeOn(Schedulers.io())  //多餘
                  .observeOn(AndroidSchedulers.mainThread())
                  .map()

多餘的線程跳躍。

二 錯誤處理

衆所周知,RxJava一大優勢就是能夠將錯誤處理集中起來到最後訂閱者的onError中統一處理,那麼如果我們在onNext、onCompleted或者onError中又拋出了異常,那麼錯誤會被catch住還是直接crash呢?我們來看看源碼吧:

private static <T> Subscription subscribe(Subscriber<? super T> subscriber, Observable<T> observable) {
    try {
        //事件源邏輯
        hook.onSubscribeStart(observable, observable.onSubscribe).call(subscriber);
        return hook.onSubscribeReturn(subscriber);
    } catch (Throwable e) {  //(1)
        Exceptions.throwIfFatal(e);//(2)
        try {
            subscriber.onError(hook.onSubscribeError(e));//(3)
        } catch (Throwable e2) {//(4)
            Exceptions.throwIfFatal(e2);
            RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2);
            hook.onSubscribeError(r);
            throw r;
        }
    }
}

我們可以看到事件源也就是我們調用onNext或者onCompleted的地方一旦發生異常,那麼就會走到(1)被Catch住,這裏至少說明你在onNext或者onCompleted中所產生的異常都會被兜住,在(3)傳遞給onError,當然SafeSubscriber做了點小魔法,不會讓onCompleted產生的錯誤傳遞給onError,因爲按照Rx規範,一個流只可能執行onCompleted和onError中之一作爲結束。

Rx規範 的保證大部分是通過SafeSubscriber這個類來實現的,我們通過普通的Observable.subscribe()進行訂閱時,會自動包裝成SafeSubscriber

如果我們在onError中處理產生了錯誤,那麼會走到(4),此時RxJava會直接把這個錯誤拋出,導致crash。

上面的(2)位置的Exceptions.throwIfFatal是一個Utils方法,它會查看當前異常類型,如果是毀滅級別的異常(如 OOM),RxJava會直接拋出來而不是交給應用邏輯去處理。

我們前面討論的大部分都是非受檢的異常,如果我們需要在響應流中調用拋出受檢異常的方法,這就會比較尷尬,因爲在Rx 1.x版本中絕大部分函數簽名都不允許拋出異常,比如我們有如下場景:

//做一個轉化的已有邏輯,需要拋出來IOException
String transform(String s) throws IOException 
//在某一個數據流中需要如下處理:
  Observable.just("Nice!")  
          .map(input -> {
                  try {
                    return transform(input);
                   } catch (Throwable t) {
                     throw Exceptions.propagate(t);
                   }
                }
      );

大部分人會推薦你使用上面的做法,使用Exceptions.propagate將一個受檢異常包裝起來然後拋出,然而這樣合理嗎?

在訂閱者收到這個Throwable之後,他需要自己收到去掉一層包圍才能看到真實的異常,顯然訂閱者需要知道整個流中是怎麼包裝這個異常的,看起來不是太cool,那試試下面的方式呢?

Observable.just("Nice!")  
          .flatMap(input -> {
                  try {
                    return Observable.just(transform(input));
                   } catch (Throwable t) {
                     return Observable.error(t);
                   }
                }
      );

完美。

三 使用背壓

反壓或者背壓是RxJava中提供的一種訂閱者和數據源進行反向溝通的渠道,通常大部分文章都會說它是爲了解決數據源發送過快而接受者來不及處理收到的消息;從這個層面來說,我一直認爲反壓貌似在Android開發中沒有什麼使用場景,因爲我們畢竟不會涉及到高併發,應該不會出現需要使用反壓的情況吧?我相信大部分人應該都有這種想法。直到我遇到下面的場景的時候,對反壓的認識就更加準確了。

有個頁面是由5個模塊組成,每個模塊都一個關聯的數據請求,我們要求進入頁面之後,從上往下加載按序加載,先加載第一個,然後等第一個加載完了然後加載第二個,如果某一個失敗,那麼停止加載剩下的模塊。那麼這個模型可以用下面的僞代碼表示:

static Observable[] dataRequest = new Observable[5];
static void go() {
    Observable.from(dataRequest)
            .subscribe(new Subscriber<Observable>() {

                @Override
                public void onStart() {
                    super.onStart();
                    request(1);
                }

                @Override
                public void onCompleted() {

                }

                @Override
                public void onError(Throwable e) {

                }

                @Override
                public void onNext(Observable observable) {
                    request(1);
                    //dorequest
                }
            });
}

這個看起來不太簡潔,而且在dorequest處其實流已經被打斷了,我們可以使用萬能的flatMap來解決:

Observable.from(dataRequest)
            .flatMap(new Func1<Observable, Observable<?>>() {
                @Override
                public Observable<?> call(Observable observable) {
                    return observable;
                }
            },1);

注意,flatMap是支持背壓的,所以我們給了背壓參數爲1。

四 串行調用

首先我們來個反例來說明一下問題:

    Subject<String,String > publishSubject = PublishSubject.create();
    publishSubject.subscribe(new Subscriber<String>() {

        private String mString = "";


        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {

        }

        @Override
        public void onNext(String s) {
            System.out.println(Thread.currentThread().getName()+" doing onNext");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" out onNext");
        }
    });


    new Thread(new Runnable() {
        @Override
        public void run() {
            publishSubject.onNext("thread1");
        }
    }).start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            publishSubject.onNext("thead2");
        }
    }).start();

  //輸出
  Thread-0 doing onNext
  Thread-1 doing onNext
  Thread-0 out onNext
  Thread-1 out onNext

我們看到了什麼?訂閱者的onNext被同時調用了,這可跟事件流定義可不一樣,我們印象裏的事件流都是一個接一個串行的,現在訂閱者居然同時收到了兩個事件。同時收到事件會有什麼問題呢?很明顯,大部分的訂閱者都是被設計成在同步模式下工作的,如果同時有多個線程調用onNext方法,那就會破壞訂閱者內部的狀態。

這就是Rx規範中的串行訪問,作爲Rx規範第一條,它的重要性不言而喻,但是有可能你使用了很久的RxJava都沒有注意到,下面是原文。

Observables must issue notifications to observers serially (not in parallel). They may issue these notifications from different threads, but there must be a formal happens-before relationship between the notifications.

在Rx中有一些操作是做線程合流(join)的,比如merge flatMap等等,想象一下,merge的多個Observable會運行在不同的線程上面,如果merge操作符不做任何特殊處理,merge操作符的訂閱者狀態就會被破壞。如果去保證onNext是被串行調用的呢?一個簡單的想法肯定首先冒出來,我給訂閱者的onNext加把鎖做同步不就可以了?比如使用synchronized來描述onNext,但是你要知道,在Rx中數據表面上是從上游往下游流動的,但是有很多數據溝通是從下游往上游走的(比如上面說的反壓),如果上游往下游發數據的過程中,下游也需要往上游溝通,那麼中間的訂閱者就很有可能發生死鎖。所以在RxJava中採用串行發送器來實現這個功能:

class EmitterLoopSerializer {
  boolean emitting;
  boolean missed;
  public void emit() {
    synchronized (this) {         
        if (emitting) {
            //暫存事件,由當前正在發送的線程事件
            missed = true;         
            return;
        }
        emitting = true;            
    }
    for (;;) {
        //儘可能多的發送事件
        synchronized (this) {  
            if (!missed) {       
                emitting = false;
                return;
            }
            missed = false;    
        }
    }
}
}

這裏其實是把鎖訪問代理到了EmitterLoopSerializer,而不是訂閱者上面,這樣就可以很好的避免死鎖的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章