RxJava 容易忽視的細節: subscribeOn() 方法沒有按照預期地運行

我們知道,subscribeOn() 方法通過接收一個 Scheduler 參數,來指定對數據的處理運行在特定的線程調度器 Scheduler 上。若多次執行 subscribeOn() 方法,則只有最初的一次起作用。

subscribeOn() 方法只有第一次調用纔有效,需要滿足以下條件:

  • 被觀察者必須是 Cold Observable。
  • 被觀察者多次調用 subscribeOn() 之後,並不意味着線程只會切換一次,而是線程多次切換之後,最終切換到第一次設置的線程。

所以, subscribeOn() 方法的調用並非一直有效。本文會通過列舉一些事例,分析其失效的原因。

一. 創建 Observable 後內部使用了多線程發射數據

使用 RxJava 創建 Observable 後,假如內部使用了多線程發射數據,會帶來什麼影響呢?

RxJava 會通過 Scheduler、subscribeOn() 來管理線程,但只有在不手動更改線程的情況下,它纔會這樣做。

通常情況下,RxJava 發射的數據會在同一個線程上,但是稍作一些變化,發射的數據來自不同的線程會怎樣呢?

public static void main(String[] args) {
    Observable.create(emitter -> {
        emitter.onNext(1);
        new Thread("main") {
            @Override
            public void run() {
                emitter.onNext(2);
                emitter.onNext(3);
                emitter.onNext(4);
                emitter.onComplete();
            }
        }.start();
    })
    .subscribeOn(Schedulers.io())
    .map(integer -> {
        log(integer + " - I want this happen on an io thread");
        return integer + "";
    })
    .subscribe(s -> log("Consume: "+s));
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

public static void log(String msg) {
    System.out.println("Current Thread Name:"+Thread.currentThread().getName() + ", "+ msg);
}

執行結果:

Current Thread Name:RxCachedThreadScheduler-1, 1 - I want this happen on an io thread
Current Thread Name:RxCachedThreadScheduler-1, Consume: 1
Current Thread Name:main, 2 - I want this happen on an io thread
Current Thread Name:main, Consume: 2
Current Thread Name:main, 3 - I want this happen on an io thread
Current Thread Name:main, Consume: 3
Current Thread Name:main, 4 - I want this happen on an io thread
Current Thread Name:main, Consume: 4

上述執行結果表明,除了 emitter 發射的 1 是在 io 線程中執行的,其餘的數字都是在 main 線程中運行的。

一旦 create 操作符中的 emitter 發射了數值,甚至在新的線程發射了值,RxJava 還是會很高興地接受這些數值並將它們進一步傳遞給流。此時 RxJava 沒有改變線程,是因爲 subscribeOn() 方法已經完成了工作,訂閱已經在其他線程上進行了。這時,沒有理由 RxJava 會再次更改線程。所以,會看到上述的運行結果。

二. Hot Observable 對 subscribeOn() 調用造成的影響

2.1 特殊的創建操作符 just

just 是一個比較“特殊”的創建操作符,just 的作用是將單個數據轉換爲發射這個單個數據的 Observable。just 類似於 fromXXX,但是 fromXXX 會將數組或 Iterable 的數據取出然後逐個發射,而 just 只是簡單地原樣發射,將數組或 Iterable 當作單個數據。另外,just 創建的不是一個 Cold Observable。

下面以 just、fromCallable 爲例:

    public static void main(String[] args) {
        System.out.println("from Just");
        Observable justObservable = Observable.just(new Random().nextInt());
        justObservable.subscribe(System.out::println);
        justObservable.subscribe(System.out::println);

        System.out.println("\nfrom Callable");

        Observable callableObservable = Observable.fromCallable(() -> new Random().nextInt());
        callableObservable.subscribe(System.out::println);
        callableObservable.subscribe(System.out::println);
    }

執行結果:

from Just
1208207476
1208207476

from Callable
774558265
1432625739

我們不難發現,上述執行結果中 just 操作符創建的 Observable 即使被訂閱多次,所產生的值依然保持不變。該值是從 Observable 外部生成的,而 Observable 僅將其存儲以供以後使用。

另外,使用 just 操作符時,不需要 subscribe 訂閱也會立即執行。

    public static void main(String[] args) {
        System.out.println("from just");
        Observable.just(getRandomInteger());
    }

    public static Integer getRandomInteger() {
        System.out.println("generating Integer");
        return new Random().nextInt();
    }

執行結果:

from just
generating Integer

上述代碼,沒有進行訂閱也執行了打印“generating Integer”,而 Cold Observable 必須使用 subscribe() 纔會生效。這就相當於 just 可以立即執行,而 fromCallable 是延遲執行。

通過比較 just 和 fromCallable 操作符,接下來我們可以總結 Hot Observable 和 Cold Observable 之間的區別。

Hot Observable Cold Observable
在外部生成 Observable 在內部生成 Observable
發生訂閱之前創建 Observable 發生訂閱之後創建 Observable

2.2 just 和 fromCallable 分別調用 subscribeOn() 會怎樣?

    public static void main(String[] args) {

        Observable.just(getRandomInteger("from just"))
                .subscribeOn(Schedulers.io())
                .subscribe(s -> log("Consume just: " + s));

        Observable.fromCallable(() -> getRandomInteger("from callable"))
                .subscribeOn(Schedulers.io())
                .subscribe(s -> log("Consume fromCallable: " + s));

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static Integer getRandomInteger(String prefix) {
        log(prefix + " generating Integer");
        return new Random().nextInt();
    }

    public static void log(String msg) {
        System.out.println("Current Thread Name:"+Thread.currentThread().getName() + ", "+ msg);
    }

執行結果:

Current Thread Name:main, from just generating Integer
Current Thread Name:RxCachedThreadScheduler-1, Consume just: 147620150
Current Thread Name:RxCachedThreadScheduler-2, from callable generating Integer
Current Thread Name:RxCachedThreadScheduler-2, Consume fromCallable: -1120243490

使用 just 操作符時,getRandomInteger() 函數在 main 函數中運行。而使用 fromCallable 時,getRandomInteger() 函數是在 io 線程中運行。

因爲 Hot Observable 是在訂閱之前就創建了 Observable,所以使用 just 操作符後,getRandomInteger() 函數的調用並沒有受到 subscribeOn() 的影響。

當然,在最後 subscribe() 中他們都切換到了 io 線程。

三. Subject 是一種特殊的存在,對 subscribeOn() 調用也會造成影響

我們先來介紹一下什麼是 Subject?Subject 和 Processor 的作用是相同的。Processor 是 RxJava 2.x 新增的類,是 Reactive Stream 標準庫中的接口,它繼承自 Flowable 支持背壓控制。而 Subject 則不支持背壓控制。

舉個 Subject 使用的例子:

        Consumer<Long> subscriber1 = new Consumer<Long>() {
            @Override
            public void accept(@NonNull Long aLong) throws Exception {
                System.out.println("subscriber1: "+aLong);
            }
        };

        Consumer<Long> subscriber2 = new Consumer<Long>() {
            @Override
            public void accept(@NonNull Long aLong) throws Exception {
                System.out.println("   subscriber2: "+aLong);
            }
        };

        Consumer<Long> subscriber3 = new Consumer<Long>() {
            @Override
            public void accept(@NonNull Long aLong) throws Exception {
                System.out.println("      subscriber3: "+aLong);
            }
        };

        Observable<Long> observable = Observable.create(new ObservableOnSubscribe<Long>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<Long> e) throws Exception {
                Observable.interval(10, TimeUnit.MILLISECONDS,Schedulers.computation())
                        .take(Integer.MAX_VALUE)
                        .subscribe(e::onNext);
            }
        }).observeOn(Schedulers.newThread());

        PublishSubject<Long> subject = PublishSubject.create();
        observable.subscribe(subject);

        subject.subscribe(subscriber1);
        subject.subscribe(subscriber2);

        try {
            Thread.sleep(20L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        subject.subscribe(subscriber3);

        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

執行結果:

subscriber1: 0
   subscriber2: 0
subscriber1: 1
   subscriber2: 1
subscriber1: 2
   subscriber2: 2
      subscriber3: 2
subscriber1: 3
   subscriber2: 3
      subscriber3: 3
subscriber1: 4
   subscriber2: 4
      subscriber3: 4
subscriber1: 5
   subscriber2: 5
      subscriber3: 5
subscriber1: 6
   subscriber2: 6
      subscriber3: 6
subscriber1: 7
   subscriber2: 7
      subscriber3: 7
subscriber1: 8
   subscriber2: 8
      subscriber3: 8
subscriber1: 9
   subscriber2: 9
      subscriber3: 9
subscriber1: 10
   subscriber2: 10
      subscriber3: 10
subscriber1: 11
   subscriber2: 11
      subscriber3: 11

可以看到,多個訂閱的 subscriber(或者說觀察者)共享同一事件。

Subject 的特殊性在於它既是 Observable 又是 Observer(Subscriber)。從 Subject 的源碼上看到,繼承自 Observable 實現 Observer。

當 Subject 作爲觀察者時,它可以訂閱目標 Cold Observable 使對方開始發送事件。同時它又作爲 Observable 轉發或者發送新的事件,讓 Cold Observable 藉助 Subject 轉換爲 Hot Observable。

Subject 並不是線程安全的,如果想要其線程安全需要調用toSerialized()方法。(在 RxJava 1.x 的時代還可以用 SerializedSubject 代替 Subject,但是在 RxJava 2.x 以後 SerializedSubject 不再是一個 public class)

RxJava 的官網稱 Subject 可以看成是一個橋樑或者代理。Subject 包含四種類型分別是 AsyncSubject、BehaviorSubject、ReplaySubject 和 PublishSubject。

用一句話分別介紹四種 Subject 的特性:

Subject 發射行爲
AsyncSubject 不論訂閱發生在什麼時候,只會發射最後一個數據
BehaviorSubject 發送訂閱之前一個數據和訂閱之後的全部數據
ReplaySubject 不論訂閱發生在什麼時候,都發射全部數據
PublishSubject 發送訂閱之後的全部數據

下面我們以 2 兩種常見的 Subject 爲例,來看看他們調用 subscribeOn() 方法後會不會起作用?

3.1 PublishSubject

Observer 只接收 PublishSubject 被訂閱之後發送的數據。如果 PublishSubject 在訂閱之前,已經執行了 onComplete() 方法,則無法發射數據。

下面的例子使用 PublishSubject 創建一個數據流,稍後向其發送值。 PublishSubject 使用 subscribeOn() 切換到不同的線程池,並不會起作用。

    public static void main(String[] args) {

        PublishSubject<Integer> subject = PublishSubject.create();

        subject.subscribeOn(Schedulers.io())
                .doOnNext(i-> log("value: "+ i+" - I want this happen on an io thread")).subscribe();

        subject.subscribeOn(Schedulers.newThread())
                .doOnNext(i-> log("value: "+ i+" - I want this happen on a new thread")).subscribe();

        subject.subscribeOn(Schedulers.computation())
                .doOnNext(i-> log("value: "+ i+" - I want this happen on a computation thread")).subscribe();

        try {
            Thread.sleep(20);
            subject.onNext(1);
            Thread.sleep(20);
            subject.onNext(2);
            Thread.sleep(20);
            subject.onNext(3);
            Thread.sleep(20);
            subject.onComplete();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void log(String msg) {
        System.out.println("Current Thread Name:"+Thread.currentThread().getName() + ", "+ msg);
    }

執行結果:

Current Thread Name:main, value: 1 - I want this happen on an io thread
Current Thread Name:main, value: 1 - I want this happen on a new thread
Current Thread Name:main, value: 1 - I want this happen on a computation thread
Current Thread Name:main, value: 2 - I want this happen on an io thread
Current Thread Name:main, value: 2 - I want this happen on a new thread
Current Thread Name:main, value: 2 - I want this happen on a computation thread
Current Thread Name:main, value: 3 - I want this happen on an io thread
Current Thread Name:main, value: 3 - I want this happen on a new thread
Current Thread Name:main, value: 3 - I want this happen on a computation thread

從執行結果上 subscribeOn() 並沒有起作用,所有的操作都是在主線程中運行。如果想達到切換線程的效果,需要讓 Subject 使用 observeOn() 替換 subscribeOn() 。

3.2 BehaviorSubject

Observer 會接收到 BehaviorSubject 被訂閱之前的最後一個數據,再接收訂閱之後發射過來的數據。如果 BehaviorSubject 被訂閱之前沒有發送任何數據,則會發送一個默認數據。

由於 BehaviorSubject 的這個特性,subscribeOn() 變得很微妙了,它可能會影響到線程的切換。例如:

    public static void main(String[] args) {

        BehaviorSubject<Integer> subject = BehaviorSubject.create();

        subject.subscribeOn(Schedulers.io())
                .doOnNext(i-> log("value: "+ i+" - I want this happen on an io thread")).subscribe();

        subject.onNext(1);

        subject.subscribeOn(Schedulers.newThread())
                .doOnNext(i-> log("value: "+ i+" - I want this happen on a new thread")).subscribe();

        subject.subscribeOn(Schedulers.computation())
                .doOnNext(i-> log("value: "+ i+" - I want this happen on a computation thread")).subscribe();

        try {
            Thread.sleep(20);
            subject.onNext(2);
            Thread.sleep(20);
            subject.onNext(3);
            Thread.sleep(20);
            subject.onComplete();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void log(String msg) {
        System.out.println("Current Thread Name:"+Thread.currentThread().getName() + ", "+ msg);
    }

執行結果:

Current Thread Name:main, value: 1 - I want this happen on an io thread
Current Thread Name:RxNewThreadScheduler-1, value: 1 - I want this happen on a new thread
Current Thread Name:RxComputationThreadPool-1, value: 1 - I want this happen on a computation thread
Current Thread Name:main, value: 2 - I want this happen on an io thread
Current Thread Name:main, value: 2 - I want this happen on a new thread
Current Thread Name:main, value: 2 - I want this happen on a computation thread
Current Thread Name:main, value: 3 - I want this happen on an io thread
Current Thread Name:main, value: 3 - I want this happen on a new thread
Current Thread Name:main, value: 3 - I want this happen on a computation thread

從執行結果上看,這段代碼在3個線程上運行。

當我們的 subject 發射第一個值時,第一個觀察者已經被訂閱。由於訂閱代碼在我們調用 onNext() 時已經完成,因此訂閱調度程序沒有任何作用。在這種情況下,當我們調用 onNext() 它類似於 PublishSubject 的工作方式。

第二和第三個觀察者都在初始 onNext() 之後訂閱。這是 BehaviorSubject 特性,對於任何新的訂閱,它將重播最後一個發射的數據。因此,對於這兩個觀察者來說,BehaviorSubject 已緩存了這個發射的值(1),並將其作爲預訂的一部分發出。這樣,將尊重訂閱調度程序,並在它提供的線程上通知觀察者。

所有後續的發射的值都發生在訂閱之後,因此,值再次與 onNext() 在同一線程上發出,類似於 PublishSubject 的工作方式。

四. timer、interval 等函數其實有默認的 Scheduler

RxJava 的某些操作符,例如:timer、interval、buffer、debounce、delay 等都支持 Scheduler ,例如:

    @CheckReturnValue
    @SchedulerSupport(SchedulerSupport.COMPUTATION)
    @NonNull
    public static Observable<Long> timer(long delay, @NonNull TimeUnit unit) {
        return timer(delay, unit, Schedulers.computation());
    }

    @CheckReturnValue
    @SchedulerSupport(SchedulerSupport.CUSTOM)
    @NonNull
    public static Observable<Long> timer(long delay, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) {
        Objects.requireNonNull(unit, "unit is null");
        Objects.requireNonNull(scheduler, "scheduler is null");

        return RxJavaPlugins.onAssembly(new ObservableTimer(Math.max(delay, 0L), unit, scheduler));
    }

通常情況下,我們不指定 Scheduler,就是使用默認的 ComputationScheduler,所以即使 subscribeOn() 指定了某個 Scheduler,也不會起作用。

    public static void main(String[] args) {

        Observable.timer(5, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .subscribe(s -> log("Consume: " + s));

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void log(String msg) {
        System.out.println("Current Thread Name:"+Thread.currentThread().getName() + ", "+ msg);
    }

執行結果:

Current Thread Name:RxComputationThreadPool-1, Consume: 0

因此,執行的結果運行在 computation() 線程上也不奇怪。

只有,在 timer 函數中指定 Scheduler,觀察者纔會運行在相應的線程。

    public static void main(String[] args) {

        Observable.timer(5, TimeUnit.MILLISECONDS,Schedulers.io())
                .subscribe(s -> log("Consume: " + s));

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void log(String msg) {
        System.out.println("Current Thread Name:"+Thread.currentThread().getName() + ", "+ msg);
    }

五. 總結

RxJava 用好不易,很多東西需要深究其源碼。

本文介紹了幾種方式,RxJava 即使調用了 subscribeOn() 方法,線程切換也不會起作用。任何細微使用線程切換的地方,都需要非常注意。雖然有時它不會使應用程序奔潰,但是它們可能會造成一些意外的結果。尚書有云:“不矜細行,終累大德。爲山九仞,功虧一簣”。我們在學習和使用過程中也不能忽視任何細節。

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