前言
RxJava
事件的發出和消費都在同一個執行緒,基於同步的觀察者模式。觀察者模式的核心是後臺處理,前臺回撥的非同步機制。要實現非同步,需要引入 RxJava
的另一個概念 - 執行緒排程器 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())
的指定,被建立的事件的內容 1
、2
、3
、4
將會在 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()的原理圖
從圖中可以看出,subscribeOn()
進行了執行緒切換的工作(圖中的 schedule...
的位置)。
subscribeOn()
的執行緒切換髮生在 OnSubscribe
中,即在它通知上一級 OnSubscribe
時,這時事件還沒有開始傳送,因此 subscribeOn()
的執行緒控制只能在事件發出的開端造成影響,即只允許一次執行緒切換。
- observeOn()的原理圖
從圖中可以看出,和 observeOn()
進行了執行緒切換的工作(圖中的 schedule...
的位置)。
observeOn()
的執行緒切換則發生在它內建的 Subscriber
中,即發生在它即將給下一級 Subscriber
傳送事件時,因此 observeOn()
控制的是它後面的執行緒,允許多次執行緒切換。
- 混合切換原理圖
最後用一張圖來解釋當多個 subscribeOn()
和 observeOn()
混合使用時,執行緒排程是怎麼發生的:
圖中共有 5
處對事件的操作,由圖中可以看出:
-
① 和 ② 兩處受第一個
subscribeOn()
影響,執行在紅色執行緒; -
③ 和 ④ 處受第一個
observeOn()
的影響,執行在綠色執行緒; -
⑤ 處受第二個
onserveOn()
影響,執行在紫色執行緒; -
而第二個
subscribeOn()
,由於在通知過程中執行緒就被第一個subscribeOn()
截斷,因此對整個流程並沒有任何影響。
注意:當使用了多個 subscribeOn() 的時候,只有第一個 subscribeOn() 起作用。
4. 延伸擴充
雖然超過一個的 subscribeOn()
對事件處理的流程沒有影響,但在流程之前卻是有用的。在前面的文章介紹 Subscriber
的時候,提到過 Subscriber
的 onStart()
可以用作流程開始前的初始化處理。
由於 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
在非同步程式設計體驗、靈活性和執行效率上領先於其他的開源框架!
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。