前言
歡迎來到深入理解 RxJava2 系列第三篇。在上一篇中,我們詳細地介紹了 Scheduler 與 Worker 的概念,並分析了ComputationScheduler
與IoScheduler
的實現,以幫助大家加深理解。本篇文章將基於 Scheduler ,來和大家分享 RxJava2 非常重要的概念:執行緒操作符。順帶提一下,本系列文章所有內容如不特別說明,均是指 Flowable
相關的概念,因為這是 RxJava2 遵循 RS 的實現。
定義
Scheduler 相關操作符
RxJava 有很多基於 Scheduler 的操作符,如timer
、interval
、debounce
等,但是筆者認為這些操作符與subscribeOn
、unsubscribeOn
、observeOn
有本質上的區別。
其他的操作符,把 Scheduler 當做了計時工具,而 Scheduler 的排程導致執行緒切換是其附帶屬性,其核心是操作符本身的特性,如:
- buffer / window 按照時間段快取資料
- throttle / debounce / throttle / skip 按照時間段取樣資料
- timer/interval 按照時間段產生資料
- delay 延遲資料
- ...
執行緒操作符
因此筆者定義狹義上的執行緒操作符,其目的是為了改變上下游的某些操作所在的執行緒。更嚴格的說法是,其目的是將上下游的某些操作由目標 Scheduler 排程執行,因為某些 Scheduler 的排程並不一定會切換執行緒,如Schedulers.trampoline()
。雖然如此,但是我們還是稱之為執行緒操作符,因為通常我們的本意是為了切換執行緒。
以下是所有的執行緒操作符:
- subscribeOn:排程上游的
Flowable
的subscribe
方法,可能會排程上游Subscription
的request
方法 - unsubscribeOn:排程上游的
Subscription
的cancel
方法 - observeOn:排程下游
Subscriber
的onNext / onError / onComplete
方法
詳解
通常subscribeOn
與observeOn
更受大家關注一些,因為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
會影響發射資料的執行緒,從而間接的影響了消費者的消費的執行緒。
但是,消費執行緒和生產執行緒依然是同一個執行緒,這裡從官網取一張示意圖:
資料產生後在傳遞給下游的過程中,是不會發生執行緒切換的,請大家謹記。
結語
筆者本想一起介紹subscribeOn
與observeOn
的,奈何洋洋灑灑地一寫便收不住,為了避免文章過長導致讀者厭倦,observeOn
以及這兩者的結合與對比留待下篇分享。
感覺大家的閱讀,歡迎關注筆者公眾號,可以第一時間獲取更新,同時歡迎留言溝通。