深入理解 RxJava2:揭祕 subscribeOn(3)

蝶翼的罪發表於2018-08-20

前言

歡迎來到深入理解 RxJava2 系列第三篇。在上一篇中,我們詳細地介紹了 Scheduler 與 Worker 的概念,並分析了ComputationSchedulerIoScheduler的實現,以幫助大家加深理解。本篇文章將基於 Scheduler ,來和大家分享 RxJava2 非常重要的概念:執行緒操作符。順帶提一下,本系列文章所有內容如不特別說明,均是指 Flowable相關的概念,因為這是 RxJava2 遵循 RS 的實現。

定義

Scheduler 相關操作符

RxJava 有很多基於 Scheduler 的操作符,如timerintervaldebounce等,但是筆者認為這些操作符與subscribeOnunsubscribeOnobserveOn有本質上的區別。

其他的操作符,把 Scheduler 當做了計時工具,而 Scheduler 的排程導致執行緒切換是其附帶屬性,其核心是操作符本身的特性,如:

  • buffer / window 按照時間段快取資料
  • throttle / debounce / throttle / skip 按照時間段取樣資料
  • timer/interval 按照時間段產生資料
  • delay 延遲資料
  • ...

執行緒操作符

因此筆者定義狹義上的執行緒操作符,其目的是為了改變上下游的某些操作所在的執行緒。更嚴格的說法是,其目的是將上下游的某些操作由目標 Scheduler 排程執行,因為某些 Scheduler 的排程並不一定會切換執行緒,如Schedulers.trampoline()。雖然如此,但是我們還是稱之為執行緒操作符,因為通常我們的本意是為了切換執行緒。

以下是所有的執行緒操作符:

  • subscribeOn:排程上游的Flowablesubscribe方法,可能會排程上游Subscriptionrequest 方法
  • unsubscribeOn:排程上游的Subscriptioncancel方法
  • observeOn:排程下游SubscriberonNext / onError / onComplete 方法

詳解

通常subscribeOnobserveOn更受大家關注一些,因為unsubscribeOn使用的場景很少。因此本文就不會再花費過多筆墨在unsubscribeOn上,而且這個操作符本身的實現就非常簡單,諸位一覽便知。

subscribeOn

subscribeOn顧名思義,改變了上游的subscribe所在的執行緒。在傳統的 Observable 中,只是改變了Observable.subscribe所在的執行緒,而在 Flowable 中不僅如此,還同樣的改變了Subscription.request所在的執行緒。

這裡就涉及到subscribeOn設計的用途,它最主要的目標是改變發射資料來源的執行緒。因此在 Observable中資料的發射,也就是耗時操作一般在subscribe所在的執行緒(這裡不考慮在onSubscribe後內部開執行緒非同步回撥的情況)。

而在 RS 的規範中資料的回撥是由消費者主動呼叫Subscription.request 來觸發的,因此在Flowable的實現中也要處理request的情況。

Asynchronous 資料來源

上面我們提到 RS 的規範中由消費者主動呼叫Subscription.request 來觸發回撥資料,但是有些資料是非同步產生的,可能在subscribe的一刻或者在那之前,譬如下面 2 個 API:

create

create 方法接受FlowableOnSubscribe作為真正的資料來源。這個方法其實相比 RxJava1 已經做了很大的限制,通過封裝了一層來支援 Backpressure。

關於此方法的細節,不再詳細介紹,筆者之前有寫過一篇文章分析過這個方法《Rx2:小create,大文章》,有興趣的讀者可以去看看。

但是即便封裝後支援了 Backpressure,背壓的邏輯更多的還是隱藏在操作符內部了,對外部的使用者還是儘量遮蔽了這些細節。FlowableEmitter 唯一能與 Backpressure 互動的介面僅是long requested();,並不能實時的響應Subscription.request

unsafeCreate / fromPublisher

這兩者是幾乎一致的,接受一個Publisher作為資料來源,外面封了一層Flowable代理該Publisher物件,通過這種方式來提供Flowable的豐富的操作符。

換種角度來看,其實這兩個方法更像 RxJava1.x 中的 create 方法。因為資料來源是來自Publisher,因此使用更加自由與隨意。

強與弱

基於上述原因,在subscribeOn還提供了第二個引數來控制request的排程。

我們看一下方法的簽名:

public final Flowable<T> subscribeOn(@NonNull Scheduler scheduler, boolean requestOn)
複製程式碼

再看一眼唯一使用該引數的地方:

void requestUpstream(final long n, final Subscription s) {
    if (nonScheduledRequests || Thread.currentThread() == get()) {
        s.request(n);
    } else {
        worker.schedule(new Request(s, n));
    }
}

複製程式碼

注意這裡nonScheduledRequests = !requestOn,該引數的作用就很明顯了。

如果requestOn = true,確保Subscription.request方法一定在目標執行緒執行。反之requestOn = false,則直接在當前執行緒執行request

我們再看一下過載的單一引數的方法:

public final Flowable<T> subscribeOn(@NonNull Scheduler scheduler) {
    ObjectHelper.requireNonNull(scheduler, "scheduler is null");
    return subscribeOn(scheduler, !(this instanceof FlowableCreate));
}
複製程式碼

這裡解釋一下 FlowableCreate 是Flowable.create方法返回的類名,也就是說除了create作為上游的 Flowable,其他都推薦用強排程的方式。為什麼單單create不可以用強排程呢。

我們用一個例子演示一下:

舉例
Flowable.<Integer>create(t -> {
    t.onNext(1);
    Thread.sleep(500);
    t.onNext(2);
    t.onNext(3);
    t.onComplete();
}, BackpressureStrategy.DROP)
// 註釋 1 .map(i -> i + 1)
// 註釋 2 .subscribeOn(Schedulers.io())
        .subscribe(new Subscriber<Integer>() {
            @Override
            public void onSubscribe(Subscription s) {
                s.request(1);
                new Thread(() -> {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ignored) {
                    }
                    s.request(2);
                }).start();
            }

            @Override
            public void onNext(Integer integer) {
                System.out.println(integer);
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("complete");
            }
        });
複製程式碼

我們在create中發射了一個 1,延時 500ms,再次發射 2、3,隨後結束,但是我們在訂閱的時候先請求了 1 個資料,隨後延時 100ms 再次請求 2個資料。

按照正常的流程,雖然資料請求延遲 100ms,但是資料發射延遲了 500ms,因而Subscriber能正確的收到3個資料:

1
2
3
complete
複製程式碼

非常棒,一切都很美好。此時我們把註釋 2 處給取消掉,再次執行結果依然同上。

此時我們應該清楚,過載的函式傳入的引數是 false。好我們再試一下,但是這次把註釋 2 處的程式碼換成:

.subscribeOn(Schedulers.io(), true)

結果:
1
complete
複製程式碼

很意外,2 和 3 去哪了?其實原因很簡單,因為我們把引數改成 true 以後,request方法要被 worker 排程後執行。

我們在《深入理解 RxJava2:Scheduler(2)》中強調過, Worker 有一個職責,保證入隊的任務是序列執行的,換言之,我們的

t -> {
    t.onNext(1);
    Thread.sleep(500);
    t.onNext(2);
    t.onNext(3);
    t.onComplete();
}
複製程式碼

是在 Worker 中執行的,因為這裡的函式沒有執行完,就無法執行後續的 request 任務。因此在資料發射過程中,上游自始至終都認為下游一開始只請求了一次資料,所以多發射的 2 與 3 就被丟棄了。

不僅如此,我們再把註釋 1 與 2 同時取消掉:

.map(i -> i + 1)
.subscribeOn(Schedulers.io())

結果:
2
complete
複製程式碼

如果讀者能理解筆者上面分享的內容,就能知道是為什麼,奧祕就在:

public final Flowable<T> subscribeOn(@NonNull Scheduler scheduler) {
    ObjectHelper.requireNonNull(scheduler, "scheduler is null");
    return subscribeOn(scheduler, !(this instanceof FlowableCreate));
}
複製程式碼

subscribeOn前面增加了map操作符後,物件就不再是FlowableCreate了,而被map封了一層。所以導致requestOn錯誤的判別為true,最終導致執行緒鎖住了request的個數。

因此subscribeOn看起來簡單,使用起來還是有不少道道的,望大家留心。

執行緒影響

上面我們提過subscribeOn會影響發射資料的執行緒,從而間接的影響了消費者的消費的執行緒。

但是,消費執行緒和生產執行緒依然是同一個執行緒,這裡從官網取一張示意圖:

深入理解 RxJava2:揭祕 subscribeOn(3)

資料產生後在傳遞給下游的過程中,是不會發生執行緒切換的,請大家謹記。

結語

筆者本想一起介紹subscribeOnobserveOn的,奈何洋洋灑灑地一寫便收不住,為了避免文章過長導致讀者厭倦,observeOn以及這兩者的結合與對比留待下篇分享。

感覺大家的閱讀,歡迎關注筆者公眾號,可以第一時間獲取更新,同時歡迎留言溝通。

深入理解 RxJava2:揭祕 subscribeOn(3)

相關文章