Android非同步框架RxJava 1.x系列(三) - 執行緒排程器Scheduler

零壹技術棧發表於2018-07-05

前言

RxJava 事件的發出和消費都在同一個執行緒,基於同步的觀察者模式。觀察者模式的核心是後臺處理,前臺回撥的非同步機制。要實現非同步,需要引入 RxJava 的另一個概念 - 執行緒排程器 Scheduler

Android非同步框架RxJava 1.x系列(三) - 執行緒排程器Scheduler

正文

在不指定執行緒的情況下,RxJava 遵循的是執行緒不變的原則。即在哪個執行緒呼叫 subscribe() 方法,就在哪個執行緒生產事件;在哪個執行緒生產事件,就在哪個執行緒消費事件。如果需要切換執行緒,就需要用到執行緒排程器 Scheduler

1. 幾種Scheduler介紹

RxJava 中,Scheduler - 排程器,相當於執行緒控制器,RxJava 通過它來指定每一段程式碼應該執行在什麼樣的執行緒。RxJava 已經內建了幾個 Scheduler ,它們已經適合大多數的使用場景:

  • Schedulers.immediate()

直接在當前執行緒執行,相當於不指定執行緒。這是預設的 Scheduler

  • Schedulers.newThread()

總是啟用新執行緒,並在新執行緒執行操作。

  • Schedulers.io()

I/O 操作(讀寫檔案、讀寫資料庫、網路資訊互動等)所使用的 Scheduler。行為模式和 newThread() 差不多,區別在於 io() 內部採用的是一個無數量上限的執行緒池,可以重用空閒的執行緒。因此多數情況下 io()newThread() 更有效率。

注意:不要把計算任務放在 io() 中,可以避免建立不必要的執行緒。

  • Schedulers.computation()

計算任務所使用的 Scheduler。這個計算指的是 CPU 密集型計算,即不會被 I/O 等操作限制效能的操作,例如圖形的計算。這個 Scheduler 使用的固定的執行緒池,大小為 CPU 核數。

注意:不要把 I/O 操作放在 computation() 中,否則 I/O 操作的等待時間會浪費 CPU。

  • AndroidSchedulers.mainThread()

Android 還有一個專用的 AndroidSchedulers.mainThread(),它指定的操作將在 Android 主執行緒執行。

2. Scheduler的執行緒切換

2.1. 單次執行緒切換

有了這幾個 Scheduler,就可以使用 subscribeOn()observeOn() 兩個方法來對執行緒進行控制了。

  • subscribeOn(): 指定 subscribe() 所發生的執行緒,即 Observable.OnSubscribe 被啟用時所處的執行緒,或者叫做事件產生的執行緒。

  • observeOn(): 指定 Subscriber 所執行在的執行緒,或者叫做事件消費的執行緒。

直接看程式碼:

Observable.just(1, 2, 3, 4)
    .subscribeOn(Schedulers.io()) // 指定 subscribe() 發生在 IO 執行緒
    .observeOn(AndroidSchedulers.mainThread()) // 指定 Subscriber 的回撥發生在主執行緒
    .subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer number) {
            Log.d(tag, "number:" + number);
        }
    });
複製程式碼

上面這段程式碼中,由於 subscribeOn(Schedulers.io()) 的指定,被建立的事件的內容 1234 將會在 IO 執行緒發出;由於 observeOn(AndroidScheculers.mainThread()) 的指定,因此 subscriber 數字的列印將發生在主執行緒

事實上,這種使用方式非常常見,它適用於多數的 『後臺執行緒取資料,主執行緒顯示』的程式策略。

以下是一個完整的例子:

int drawableRes = ...;
ImageView imageView = ...;
Observable.create(new OnSubscribe<Drawable>() {
    @Override
    public void call(Subscriber<? super Drawable> subscriber) {
        Drawable drawable = getTheme().getDrawable(drawableRes));
        subscriber.onNext(drawable);
        subscriber.onCompleted();
    }
})
// 指定事件發出,即圖片讀取發生在 IO 執行緒
.subscribeOn(Schedulers.io())
// 指定事件消費 - 回撥,即頁面圖片渲染髮生在主執行緒
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Drawable>() {
    @Override
    public void onNext(Drawable drawable) {
        imageView.setImageDrawable(drawable);
    }

    @Override
    public void onCompleted() {
    }

    @Override
    public void onError(Throwable e) {
        Toast.makeText(activity, "Error!", Toast.LENGTH_SHORT).show();
    }
});
複製程式碼

這樣的好處是,載入圖片的過程發生在 IO 執行緒,而設定圖片則發生在了主執行緒。這就意味著,即使載入圖片耗費了幾十甚至幾百毫秒的時間,也不會造成介面上的絲毫卡頓。

2.2. 多次執行緒切換

上面介紹到可以利用 subscribeOn() 結合 observeOn() 來實現執行緒控制,讓事件的產生和消費發生在不同的執行緒。在瞭解了 map()flatMap() 等變換方法後,一個問題就產生了 - 能不能多切換幾次執行緒?

因為 observeOn() 指定的是 Subscriber 的執行緒,而這個 Subscriber 並不是 subscribe() 引數中的 Subscriber ,而是 observeOn() 執行時,當前 Observable 所對應的 Subscriber,即它的直接下級 Subscriber

也就是說,observeOn() 指定的是它之後的操作所在的執行緒。因此如果有多次切換執行緒的需求,只要在每個想要切換執行緒的位置呼叫一次 observeOn() 即可。

直接檢視示例程式碼:

Observable.just(1, 2, 3, 4) 
    // 事件發出的 IO 執行緒,由 subscribeOn() 指定
    .subscribeOn(Schedulers.io())
    // 新執行緒,由 observeOn() 指定
    .observeOn(Schedulers.newThread())
    .map(mapOperator) 
    // IO 執行緒,由 observeOn() 指定
    .observeOn(Schedulers.io())
    .map(mapOperator2) 
    // Android 主執行緒,由 observeOn() 指定
    .observeOn(AndroidSchedulers.mainThread)
    .subscribe(subscriber);
複製程式碼

上面的程式碼,通過 observeOn() 的多次呼叫,程式實現了執行緒的多次切換。不過,不同於 observeOn()的是,subscribeOn() 的位置放在哪裡都可以,但它是隻能呼叫一次的。

3. Scheduler的實現原理

其實,subscribeOn()observeOn() 的內部實現,也是用的 lift() (見上文),具體看圖(不同顏色的箭頭表示不同的執行緒):

  • subscribeOn()的原理圖

Android非同步框架RxJava 1.x系列(三) - 執行緒排程器Scheduler

從圖中可以看出,subscribeOn() 進行了執行緒切換的工作(圖中的 schedule... 的位置)。

subscribeOn() 的執行緒切換髮生在 OnSubscribe 中,即在它通知上一級 OnSubscribe 時,這時事件還沒有開始傳送,因此 subscribeOn() 的執行緒控制只能在事件發出的開端造成影響,即只允許一次執行緒切換。

  • observeOn()的原理圖

Android非同步框架RxJava 1.x系列(三) - 執行緒排程器Scheduler

從圖中可以看出,和 observeOn() 進行了執行緒切換的工作(圖中的 schedule... 的位置)。

observeOn() 的執行緒切換則發生在它內建的 Subscriber 中,即發生在它即將給下一級 Subscriber 傳送事件時,因此 observeOn() 控制的是它後面的執行緒,允許多次執行緒切換。

  • 混合切換原理圖

最後用一張圖來解釋當多個 subscribeOn()observeOn() 混合使用時,執行緒排程是怎麼發生的:

Android非同步框架RxJava 1.x系列(三) - 執行緒排程器Scheduler

圖中共有 5 處對事件的操作,由圖中可以看出:

  1. ① 和 ② 兩處受第一個 subscribeOn() 影響,執行在紅色執行緒

  2. ③ 和 ④ 處受第一個 observeOn() 的影響,執行在綠色執行緒

  3. ⑤ 處受第二個 onserveOn() 影響,執行在紫色執行緒

  4. 而第二個 subscribeOn() ,由於在通知過程中執行緒就被第一個 subscribeOn() 截斷,因此對整個流程並沒有任何影響。

注意:當使用了多個 subscribeOn() 的時候,只有第一個 subscribeOn() 起作用。

4. 延伸擴充

雖然超過一個的 subscribeOn() 對事件處理的流程沒有影響,但在流程之前卻是有用的。在前面的文章介紹 Subscriber 的時候,提到過 SubscriberonStart() 可以用作流程開始前的初始化處理

由於 onStart() 在 subscribe() 發生時就被呼叫了,因此不能指定執行緒,而是隻能執行在 subscribe() 被呼叫時的執行緒。這就導致如果 onStart() 中含有對執行緒有要求的程式碼(例如:在介面上顯示一個 ProgressBar,這必須在主執行緒執行),將會有執行緒非法的風險,因為無法預測 subscribe() 會在什麼執行緒執行。

Subscriber.onStart() 相對應的,有一個方法 Observable.doOnSubscribe()。它和 Subscriber.onStart() 同樣是在 subscribe() 呼叫後而且在事件傳送前執行,但區別在於它可以指定執行緒。預設情況下,doOnSubscribe() 執行在 subscribe() 發生的執行緒;而如果在 doOnSubscribe() 之後有 subscribeOn() 的話,它將執行在離它最近的 subscribeOn() 所指定的執行緒。

示例程式碼如下:

Observable.create(onSubscribe)
    .subscribeOn(Schedulers.io())
    .doOnSubscribe(new Action0() {
        @Override
        public void call() {
            // 需要在主執行緒執行
            progressBar.setVisibility(View.VISIBLE); 
        }
    })
    .subscribeOn(AndroidSchedulers.mainThread()) 
    // 指定主執行緒
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(subscriber);
複製程式碼

上面的程式碼,在 doOnSubscribe() 的後面跟一個 subscribeOn() ,就能指定特定工作的執行緒了!

小結

RxJava 的提供的各種事件及事件轉換模型,以及基於轉換的執行緒排程器,結合觀察者模式,使得 RxJava 在非同步程式設計體驗、靈活性和執行效率上領先於其他的開源框架!


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章