RxJava - 小心 observeOn 的陷阱

在 Android 中使用 RxJava 經常會用到 observeOn 這個操作符來完成線程的切換,比如網絡請求之后切換到“主線程”,通常會這么寫:

  Observable.observeOn(AndroidSchedulers.mainThread())

然而我遇到一個奇怪的問題,onNext 有時候會丟失,先描述一下場景。我用 Retrofit + RxJava 來進行網絡請求,我可以直接拿到一個 Observable,就像下面這樣:

// 定義 Retrofit 接口
public interface FeedApi {    
  @GET("feeds")    
  Observable<List<Feed>> feeds();
}
// 省略部分代碼
RestApi restApi = retrofit.create(RestApi.class);

Observable<List<Feed>> observable = restApi.feeds()
      .subscribeOn(Schedulers.io())       
      .observeOn(AndroidSchedulers.mainThread());

observable.subscribe(...);

目前來看一切正常,我為了更好的用戶體驗,加入了緩存,當網絡有問題的時候,會顯示緩存數據,用戶不會看到一個空白的頁面。緩存的方式多種多樣,比如 Realm,同樣支持 RxJava,我依然可以拿到一個 Observable,然后我使用 concat 把來自緩存的 Observable 和來自網絡的 Observable 合并為一個 Observable 對外提供,就像下面這樣:

Observable<List<Feed>> fromNetwork = ...
Observable<List<Feed>> fromCache = ...

Observable<List<Feed>> observable = Observable.concat(fromCache, fromNetwork)
      .subscribeOn(Schedulers.io())       
      .observeOn(AndroidSchedulers.mainThread());

observable.subscribe(...);

合并之后的 Observable 繼續使用 subscribeOnobserveOn 來完成線程的切換。恩,看起來很完美,我想象中運行效果是這樣的:

假設已經存在緩存的情況下
網絡正常的時候,fromCache 從緩存拿到數據發送給 Subscriber,顯示到頁面上,然后 fromNetwork 從服務器拿到最新的數據,繼續發送給 Subscriber,刷新頁面為最新的數據,Subscribe 會經歷 onNext() -> onNext() -> onCompleted()
網絡異常的情況,fromCache 從緩存拿到數據發送給 Subscriber,顯示到頁面上,然后 fromNetwork 發生異常,Subscriber 收到 Error,可以提示用戶網絡異常,但是頁面上會顯示緩存的數據,用戶不會看到空白頁面,Subscriber 會經歷 onNext() -> onError()

我運行之后,在網絡正常的情況下,能顯示數據,當我把網絡關閉的時候,依然能顯示數據,數據是從緩存加載的,還不錯,和我想象的一樣。但是當我反復多次打開頁面的時候,發現一個奇怪的現象,頁面有時候會顯示空白,有時候會顯示緩數據,但是都會提示網絡異常的信息,從 Subscriber 的角度講,就是 Subscriber 有時候會 onNext() -> onError(),有時候只會 onError(),緩存數據的那次 onNext() 丟失了!

為了搞清楚這個問題,我做了這樣的測試,在 subscribe 之前各個環節加上 doOnNext,觀察會不會執行。

observable        
  .doOnNext(s -> log("1 doOnNext: " + s))                    
  .subscribeOn(Schedulers.io())  
  .doOnNext(s -> log("2 doOnNext: " + s))            
  .observeOn(AndroidSchedulers.mainThread())
  .doOnNext(s -> log("3 doOnNext: " + s))
  .subscribe(...);

// 輸出結果
1 doOnNext: test
2 doOnNext: test

第三個居然沒有打印,說明 observeOn 之后的 onNext() 沒有執行,確定了問題范圍之后,我嘗試從 observeOn 的源碼尋找線索。我發現 observeOn 有幾個重載的方法,其中有一個參數叫 delayError,看名字是“延遲錯誤”。

public final Observable<T> observeOn(Scheduler scheduler, boolean delayError);

注釋對這個參數的描述是:

indicates if the onError notification may not cut ahead of onNext notification on the other side of the scheduling boundary. If true a sequence ending in onError will be replayed in the same order as was received from upstream

大概說 delayError 決定了 onError 會不會切斷 onNext 通知,貌似和我的問題有些關系,為了弄清楚具體情況,還是看源碼。
observeOn 基于 lift 操作符,lift 需要一個 OperatorOperator 可以將 下游 Subscriber 進行“代理”,返回一個 “代理” Subscriber 對象,“代理”對象可以收到 onNextonErroronCompleted,然后可以進行一些處理,再選擇性的發送給 下游 Subscriber

public final Observable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {    
  if (this instanceof ScalarSynchronousObservable) {
    return ((ScalarSynchronousObservable<T>)this).scalarScheduleOn(scheduler);    
  }    
  // OperatorObserveOn 是關鍵
  return lift(new OperatorObserveOn<T>(scheduler, delayError, bufferSize));
}

我們繼續看 OperatorObserveOn

public final class OperatorObserveOn<T> implements Operator<T, T> {

    ...

    @Override
    public Subscriber<? super T> call(Subscriber<? super T> child) {
        // 創建一個 Subscriber “代理”
        ObserveOnSubscriber<T> parent = new ObserveOnSubscriber<T>(scheduler, child, delayError, bufferSize);
        parent.init();
        return parent;
    }

    // Subscriber 的代理,它會代理 下游的 Subscriber 收到事件
    // 然后在 Scheduler 的線程中再轉發給 下游的 Subscriber,就完成了線程的切換
    static final class ObserveOnSubscriber<T> extends Subscriber<T> implements Action0 {

        @Override
        public void onNext(final T t) {
            // 代理對象先收到 onNext
            if (isUnsubscribed() || finished) {
                return;
            }
            // 把收到的數據放到一個隊列中
            if (!queue.offer(NotificationLite.next(t))) {
                onError(new MissingBackpressureException());
                return;
            }
            // 通知 Scheduler,Scheduler 會回調 call 方法
            schedule();
        }

        @Override
        public void onCompleted() {
            if (isUnsubscribed() || finished) {
                return;
            }
            // 標記 流 已經結束
            finished = true;
            // 通知 Scheduler,Scheduler 會回調 call 方法
            schedule();
        }

        @Override
        public void onError(final Throwable e) {
            if (isUnsubscribed() || finished) {
                RxJavaHooks.onError(e);
                return;
            }
            // 保存 error
            error = e;
            // 標記 流 已經結束
            finished = true;
            // 通知 Scheduler,Scheduler 會回調 call 方法
            schedule();
        }

        protected void schedule() {
            if (counter.getAndIncrement() == 0) {
                // 把 this 當做一個 callback,Scheduler 會調用 call 方法。
                recursiveScheduler.schedule(this);
            }
        }

        // only execute this from schedule()
        @Override
        public void call() {
            // 從 Scheduler 中調用,此時已經在 Scheduler 的線程中,比如 Android 的主線程中
            long missed = 1L;
            long currentEmission = emitted;

            final Queue<Object> q = this.queue; // 保存 next 數據的隊列
            final Subscriber<? super T> localChild = this.child;    // 下游的 Subscriber

            for (;;) {
                long requestAmount = requested.get();

                while (requestAmount != currentEmission) {
                    boolean done = finished;
                    // 從隊列中取數據,就是 onNext 的時候放到隊列中的
                    Object v = q.poll();
                    // 判斷是不是 null
                    boolean empty = v == null;
                    // 這里是關鍵,檢查是否需要中斷 onNext
                    if (checkTerminated(done, empty, localChild, q)) {
                        // 中斷,最終的 Subscriber 不會收到 onNext
                        return;
                    }

                    if (empty) {
                        break;
                    }

                    // 轉發給 下游的 Subscriber
                    localChild.onNext(NotificationLite.<T>getValue(v));

                    ...
                }
            }
        }

        // 檢查是否需要中斷 onNext
        boolean checkTerminated(boolean done, boolean isEmpty, Subscriber<? super T> a, Queue<Object> q) {
            if (a.isUnsubscribed()) {
                q.clear();
                return true;
            }

            if (done) {
                // 如果 流 已經結束,執行了 onCompleted 或者 onError
                if (delayError) {
                    // 如果是延遲錯誤,delayError 默認是 false
                    if (isEmpty) {
                        // 如果沒有 onNext 的數據了
                        Throwable e = error;
                        try {
                            if (e != null) {
                                // 如果存在 error,直接轉發給 下游的 Subscriber
                                a.onError(e);
                            } else {
                                // 如果是正常結束,也是直接轉發給 下游的 Subscriber
                                a.onCompleted();
                            }
                        } finally {
                            recursiveScheduler.unsubscribe();
                        }
                    }
                } else {
                    // 如果沒有延遲錯誤,這也是默認的情況
                    Throwable e = error;
                    if (e != null) {
                        // **** 如果已經有 error 了,把隊列里的 onNext 情況,也就是 onNext 的數據丟失了!
                        q.clear();
                        try {
                            // **** 把 error 發送給 下游的 Subscriber
                            a.onError(e);
                        } finally {
                            recursiveScheduler.unsubscribe();
                        }
                        return true;
                    } else
                    if (isEmpty) {
                        // 如果沒有 error,并且沒有 onNext 的數據了,直接完成
                        try {
                            a.onCompleted();
                        } finally {
                            recursiveScheduler.unsubscribe();
                        }
                        return true;
                    }
                }

            }

            return false;
        }
    }
}

通過上面對源碼的分析,可以確定這個問題出現的原因了,上面注釋中 **** 標記的地方就是問題所在:

if (e != null) {
    // **** 如果已經有 error 了,把隊列里的 onNext 情況,也就是 onNext 的數據丟失了!
    q.clear();
    try {
        // **** 把 error 發送給 下游的 Subscriber
        a.onError(e);
    } finally {
        recursiveScheduler.unsubscribe();
    }
    return true;
} 

默認 delayErrorfalse,就會走上面這段代碼,如果有 error 就會把隊列中還未下發給 下游 SubscriberonNext 數據清空,從而導致了 Subscriber 沒有收到 onNext,直接收到了 onError

為什么這個問題不是必現的呢,因為這有一個條件,就是出現 onError 的時候,onNext 的數據還沒有被消費完,隊列中還有數據的情況下才會丟失,也就是 Scheduler 的處理速度跟不上生產速度,這很好理解,因為我們用的是 AndroidSchedulers.mainThread() ,Android 的主線程也就是 Main Lopper,是一個單線程模型,只能一個個的處理,不能并行,而且 Android 中的各種事件,包括 View 的繪制都要通過 Main Looper 來完成,如果 Looper 中積壓的消息太多,那么新消息就不會被及時的處理,這時候我們的 fromCache 把緩存數據發送給 Subscriber,就會由于 Looper 中積壓的消息過多,Scheduler 不能立刻執行 ObserveOnSubscribercall 方法,從而不能立刻被 下游的 Subscriber 接收到,與此同時 fromNetowrk 發生了 error,當 Looper 處理到消息的時候,也就是 ObserveOnSubscribercall 方法被調用的時候,這時候隊列中存在一個 fromCache 的數據,并且 error 也存在,就發生了上面源碼分析的情況,導致隊列被清空,onNext 沒有執行,直接執行了 onError

怎么解決這個問題?答案已經明確了,就是顯式的傳遞 delayErrortrue

Observable.observeOn(AndroidSchedulers.mainThread(), true)

這個問題之前有人在 GitHub 上提過 Issue,并且在這個 Pull 3544 中解決,也就是增加了 delayError,在此之前是沒有這個參數的。

明白了原因之后,這個問題其實和 concat 沒有什么關系,不用 concat 依然可能出現 onNext 丟失,由于 Main Looper 我們不好去控制,可以通過一個簡單的 Java 程序來模擬這種情況。

public static void main(String[] args) {

    // 用一個 單線程的線程池 來模擬 Android 的主線程
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Scheduler androidMainThread = Schedulers.from(executor);

    Observable<String> observable = Observable.create(subscriber -> {
        // 發送數據
        subscriber.onNext("text");
        // 一秒后發送 error
        sleep(1000);
        subscriber.onError(new RuntimeException());
    });

    // 完成線程切換,然后訂閱
    observable
            .subscribeOn(Schedulers.io())
            .observeOn(androidMainThread)
            .subscribe(s -> {
                // 收到 onNext 數據
                System.out.println("onNext: " + s);
            }, throwable -> {
                // 收到 onError
                System.out.println("onError: " + throwable);
            });
}

static void sleep(int millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

輸出結果:

onNext: text
onError: java.lang.RuntimeException

輸出結果是正常的,既收到了 onNext 又收到了 onError,因為 androidMainThread 中沒有其他的任務要處理,Observable 發送 onNext 之后會 sleep 一秒鐘之后發送 onErrorandroidMainThread 在這一秒鐘之內完全可以把 onNext 的數據消費掉,下面模擬 androidMainThread 中有任務處理的情況。

// 主線程有其他任務處理,會阻塞 兩秒
executor.execute(() -> sleep(2000));

// 完成線程切換,然后訂閱
observable
        .subscribeOn(Schedulers.io())
        .observeOn(androidMainThread)
        .subscribe(s -> {
            // 收到 onNext 數據
            System.out.println("onNext: " + s);
        }, throwable -> {
            // 收到 onError
            System.out.println("onError: " + throwable);
        });

輸出結果:

onError: java.lang.RuntimeException

會發現只有 onErroronNext 丟失了,因為 Observable 只是 sleep 了一秒就發送了 onError,而 androidMainThread 的 線程 正在執行一個需要耗時兩秒的任務,執行完之后,ObserveOnSubscriber中的隊列存在一個 onNext 數據和一個 error,這就會發生之前分析的情況,隊列被清空,直接發送了 onError。我們再試試 顯式 傳遞 delayError 的情況。

// 主線程有其他任務處理,會阻塞 兩秒
executor.execute(() -> sleep(2000));

// 完成線程切換,然后訂閱
observable
        .subscribeOn(Schedulers.io())
        .observeOn(androidMainThread, true) // delayError 為 true
        .subscribe(s -> {
            // 收到 onNext 數據
            System.out.println("onNext: " + s);
        }, throwable -> {
            // 收到 onError
            System.out.println("onError: " + throwable);
        });

輸出結果:

onNext: text
onError: java.lang.RuntimeException

會發現 onNext 收到了,這也證明了上面源碼分析的邏輯是正確的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容