RxJava 觀察繫結和事件傳送流程及其中的執行緒切換分析

PandaQ發表於2020-04-06

本文的所有分析都是基於 RxJava2 進行的。以下的 RxJava 指 RxJava2 閱讀本文你將會知道:

  • RxJava 的觀察繫結和事件傳送過程
  • RxJava 觀察繫結和事件傳送過程中的執行緒切換

從 RxJava1.0 到 RxJava2.0,在專案開發中已經使用了很長時間這個庫了。鏈式呼叫,絲滑的執行緒切換很香,但是如果沒弄清楚其中的奧妙很容易掉進執行緒排程的坑裡。這篇文章我們就來對 RxJava 的訂閱過程、時間傳送過程、執行緒排程進行分析

訂閱和事件流

先說結論

  • 按著程式碼書寫順序,事件自上向下傳送
  • 訂閱從 subscribe() 開始自下向上訂閱,這也是整個事件流的起點,當訂閱開始整個操作才會生效執行
  • 訂閱完成後才會傳送事件

圖解

為了更便於理解訂閱的流轉方向,我將Observable呼叫 subscribe() 訂閱描述為了 Observer beSubscribed()

訂閱及資料傳送

原始碼分析

Observabe 建立過程

此過程對應圖中黑色箭頭部分,以操作符中的map()操作為例:

   @CheckReturnValue
   @SchedulerSupport(SchedulerSupport.NONE)
   public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
       ObjectHelper.requireNonNull(mapper, "mapper is null");
       return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
   }
複製程式碼

呼叫map操作符時,RxJavaPliguns 會註冊一個新的 ObservableMap 物件,檢視其它操作符會發現都有對應的 Observable 物件產生。同時,上游的 Observabe會作為 source 引數傳入賦值給這個新的 Observablesource屬性。層層向下,可以對這個新生成的 Observable又可以繼續使用操作符。

訂閱過程:

當呼叫最後一個 Observablesubscribe() 方法時,即開始訂閱過程。此過程對應圖中紅色箭頭部分

   @SchedulerSupport(SchedulerSupport.NONE)
   @Override
   public final void subscribe(Observer<? super T> observer) {
       ObjectHelper.requireNonNull(observer, "observer is null");
       try {
           observer = RxJavaPlugins.onSubscribe(this, observer);

           ObjectHelper.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins");

           subscribeActual(observer);
       } catch (NullPointerException e) { // NOPMD
           throw e;
       } catch (Throwable e) {
           Exceptions.throwIfFatal(e);
           // can't call onError because no way to know if a Disposable has been set or not
           // can't call onSubscribe because the call might have set a Subscription already
           RxJavaPlugins.onError(e);

           NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS");
           npe.initCause(e);
           throw npe;
       }
   }
複製程式碼

在呼叫subscribe(Observer) 時實際上會去呼叫各個 Observable實現子類中的 subscribeActual() 方法:

   @Override
   public void subscribeActual(Observer<? super U> t) {
       source.subscribe(new MapObserver<T, U>(t, function));
   }
複製程式碼

而在這個subscribeActual() 方法也很簡單,呼叫了 source 去訂閱一個新生成的 Observer 物件,同時這個新的MapObserver會將呼叫subscribe()時傳入的 observer,賦值給downstream屬性。這樣每一級訂閱都會將上級的 Observable、本級生成的 Observer、訂閱下級傳入的Observer聯絡起來,直到達到 Observable 最初建立的地方整個訂閱過程結束。

事件傳送過程:

此過程對應圖中綠色箭頭部分Observable 事件起點建立有很多中操作符,他們都會建立出最初傳送的事件/資料,以 ObservableCreate為例:

   @Override
   protected void subscribeActual(Observer<? super T> observer) {
       CreateEmitter<T> parent = new CreateEmitter<T>(observer);
       observer.onSubscribe(parent);

       try {
           source.subscribe(parent);
       } catch (Throwable ex) {
           Exceptions.throwIfFatal(ex);
           parent.onError(ex);
       }
   }
複製程式碼

訂閱時會呼叫source.subscrebe(parent),而這個source 又是從哪兒來的呢?

   public ObservableCreate(ObservableOnSubscribe<T> source) {
       this.source = source;
   }
複製程式碼
   Observable.create(object : ObservableOnSubscribe<String> {
          override fun subscribe(emitter: ObservableEmitter<String>) {
               emitter.onNext("data")
          }

   })
複製程式碼

從程式碼中我們可以看出,這個 source 即為我們建立時傳入的 ObservableOnSubscribe,因此emitter.onNext("data")即是事件傳送的起點。我們再繼續看emitteronNext() 做了什麼:

        @Override
        public void onNext(T t) {
            if (t == null) {
                onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
                return;
            }
            if (!isDisposed()) {
                observer.onNext(t);
            }
        }
複製程式碼

原始碼中現實呼叫了observer.onNext(),而這個observer 則是前面訂閱過程中 source.subscribe(new MapObserver<T, U>(t, function)) 傳入的那個 observer,從而將事件傳送到了下一級,下一級的 Observer 同樣在 onNext() 將事件傳送到更下一級,一直到最終我們 subscribe()時傳入的那個Observer 例項完畢。

執行緒排程

事件訂閱傳送流程通過上面的文章基本已經能夠摸清了,我們接下來關注另一個重點 執行緒排程問題。

排程方式

RxJava 中執行緒變換通過 subscribeOn()observeOn()兩個操作來進行。其中 subscribeOn()改變的是訂閱執行緒的執行執行緒,即事件發生的執行緒。observeOn()改變的是事件結果觀察者回撥所線上程,即 onNext()方法所在的執行緒。

舉個例子
使用 RxJava + Retrofit 進行網路請求時,用 RxJava 管理網路請求過程的執行緒切換。subscribeOn()指定的是網路請求的執行緒,observeOn()指定的是網路請求後事件流的執行執行緒。

原始碼分析

前面說過,每次操作符的使用,RxJava 都會生成一個對應的新的 Observable物件。observeOn()subscribeOn()也不例外。執行緒排程的核心邏輯都在 ObservableSubscribeOnObservableObserveOn兩個類中

subscribeOn()過程

  @CheckReturnValue
  @SchedulerSupport(SchedulerSupport.CUSTOM)
  public final Observable<T> subscribeOn(Scheduler scheduler) {
      ObjectHelper.requireNonNull(scheduler, "scheduler is null");
       return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
   }
複製程式碼

呼叫 subscribeOn() 時會產生一個新的ObservableSubscribeOn並把當前這個Observable 和傳入的 Scheduler作為引數傳入。前面分析過當最終呼叫 subscribe()時會引起整個觀察鏈的 Observable 自下而上呼叫 subscribe(),而這個subscribe()方法中實際為呼叫抽象類 Observable的各個實現子類的 subscribeActual()方法 。

   @Override
   public void subscribeActual(final Observer<? super T> observer) {
       final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(observer);

       observer.onSubscribe(parent);

       parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
   }
複製程式碼

主要看這句 scheduler.scheduleDirect(new SubscribeTask(parent));,SubscribeTask 前面內容已經分析過,就是呼叫上級 Observable 來訂閱生成的這個 SubscribeOnObserver

   @NonNull
   public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
       final Worker w = createWorker();

       final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

       DisposeTask task = new DisposeTask(decoratedRun, w);

       w.schedule(task, delay, unit);

       return task;
   }
複製程式碼

scheduleDirect 方法,會使用傳入的 scheduler 在指定的執行緒建立一個 Worker 物件來執行SubscribeTask,從而達到了切換訂閱執行緒的目的。所以多個subscribeOn()疊加時,最終執行緒還是會回到最後執行的(程式碼第一次出現的)subscribeOn() 指定的執行緒。

observeOn()過程

呼叫 observeOn(Scheduler) 方法,會呼叫內部的同名方法生成一個新的 ObservableObserveOn物件,並把當前這個Observable 和傳入的 Scheduler作為引數傳入。訂閱過程與ObservableSubscribeOn不一樣,會直接在當前執行緒呼叫上級Observable訂閱自己,,我們主要看ObservableObserveOnObserveOnObserver是如何排程結果資料傳送的執行緒的。

       @Override
       public void onNext(T t) {
           if (done) {
               return;
           }

           if (sourceMode != QueueDisposable.ASYNC) {
               queue.offer(t);
           }
           schedule();
       }

       void schedule() {
           if (getAndIncrement() == 0) {
               worker.schedule(this);
           }
       }
複製程式碼

從原始碼中可以發現,最終會使用 worker 去向下游傳送事件。這個 worker就是我們observeOn() 方法中指定的執行緒建立的 worker。從而達到切換執行緒的目的,由於事件又是自上而下的,所以每次切換都能在下游事件中感受到執行緒的變化。

日誌分析

subscribeOn()observeOn()放一起來說不太容易說明白其中的執行緒變換,我先看看單獨使用其中的一個操作符的時候,導致的執行緒變化。

僅呼叫 subscribeOn() 排程執行緒

Observable.just("Data")
                .map {
                    Log.d("Map 1", Thread.currentThread().name)
                    return@map it
                }
                .subscribeOn(Schedulers.io()) 
                .doOnSubscribe {
                    Log.d("doOnSubscribe 1 ", Thread.currentThread().name)
                }
                .map {
                    Log.d("Map 2 ", Thread.currentThread().name)
                    return@map it
                }
                .subscribeOn(Schedulers.newThread())
                .doOnSubscribe {
                    Log.d("doOnSubscribe 2 ", Thread.currentThread().name)
                }
                .map {
                    Log.d("Map 3 ", Thread.currentThread().name)
                    return@map it
                }
                .subscribe(object : Observer<String> {
                    override fun onComplete() {

                    }

                    override fun onSubscribe(d: Disposable) {
                        Log.d("onSubscribe", Thread.currentThread().name)
                    }

                    override fun onNext(t: String) {
                        Log.d("onNext", Thread.currentThread().name)
                    }

                    override fun onError(e: Throwable) {
                        e.printStackTrace()
                    }

                })
複製程式碼

執行結果:

日誌列印
從日誌可以看出:

  • 1、訂閱是自下向上的(onSubscribe -->doOnSubscribe 2 -->doOnsubscribe 1)
  • 2、自下向上看,每次呼叫 subscribeOn 訂閱執行緒將會發生改變,直到下次呼叫 subscribeOn
  • 3、事件是自上向下傳遞的(Map 1 --> Map 2 --> Map 3 --> onNext),且所線上程為最後一次執行緒切換後所在的執行緒 RxCachedThreadScheduler-1

僅呼叫 subscribeOn() 排程執行緒

        Observable.just("Data")
                .map {
                    Log.d("Map 1", Thread.currentThread().name)
                    return@map it
                }
//                .doOnSubscribe {
//                    Log.d("doOnSubscribe 1 ", Thread.currentThread().name)
//                }
//                .subscribeOn(Schedulers.io())
                .observeOn(Schedulers.newThread())
                .map {
                    Log.d("Map 2 ", Thread.currentThread().name)
                    return@map it
                }
//                .doOnSubscribe {
//                    Log.d("doOnSubscribe 2 ", Thread.currentThread().name)
//                }
//                .subscribeOn(Schedulers.newThread())
                .observeOn(Schedulers.newThread())
                .map {
                    Log.d("Map 3 ", Thread.currentThread().name)
                    return@map it
                }
                .subscribe(object : Observer<String> {
                    override fun onComplete() {

                    }

                    override fun onSubscribe(d: Disposable) {
                        Log.d("onSubscribe", Thread.currentThread().name)
                    }

                    override fun onNext(t: String) {
                        Log.d("onNext", Thread.currentThread().name)
                    }

                    override fun onError(e: Throwable) {
                        e.printStackTrace()
                    }

                })
複製程式碼

執行結果:

日誌列印
從日誌可以看出:

  • 1、事件傳送是正常的自上向下(Map 1 --> Map 2 --> Map 3 --> onNex)
  • 2、自上向下,每次呼叫 observeOn 觀察結果回撥執行緒都將切換一次(main -->RxNewThreadScheduler-1 -->RxNewThreadScheduler-2)

混合使用排程執行緒

我們把上述程式碼中註釋部分都開啟,得到的日誌如下:

日誌列印
通過上面的三次日誌列印我們可以看出:

訂閱鏈的日誌自下而上列印完畢後,再自上而下列印觀察結果。subscribeOn 會切換執行緒,並不是像有的文章所說只有第一次指定執行緒(即自下而上的最後一次)有效。第一次有效只是我們的錯覺,因為訂閱是自下而上的,不管前面的執行緒怎樣切換追蹤都會切換到 subscribeOn第一次指定執行緒(即自下而上的最後一次)。我們在回撥結果中未進行執行緒切換操作時,只能感知到這一次執行緒切換 (Map1 與 doOnSubscribe 1 所線上程一致)。observeOn的每次指定執行緒都會讓事件流切換到對應的執行緒中去。完整的事件訂閱和傳送流程如下圖所示,從我們呼叫 subscribe()將觀察者和觀察物件關聯起來開始,subscribe() 中傳入的 Observer 的 onNextonError結束,形成了一個逆時針的 n 形的鏈條。右邊部分的觀察鏈中,每次 subscribeOn 都會切換觀察執行緒。左邊部分的事件傳送鏈,會從觀察鏈的最後一次指定的執行緒開始傳送事件,每次呼叫 observeOn都會指定新的事件傳送執行緒。

圖解

參照上面的原始碼和日誌分析,再結合本圖相信大家會對 RxJava 的現場排程有一個更立體的認識

RxJava2 執行緒切換流程

相關文章