準確來講,RxAndroid 是隸屬於 ReactiveX 組織的,JakeWharton 作為參與者,貢獻了大量的程式碼(從 git 提交歷史記錄可查詢到),而且這個框架短小精悍,不至於像 RxJava 那麼龐大,讓人望而卻步,非常值得一讀,因此將她歸為【拆 JakeWharton 系列】之一,這系列陸續創造中,歡迎關注。
(一) 你將獲得什麼
每個框架有每個框架的使命,閱讀原始碼,可以挖掘相應的技術點,閱讀原始碼的樂趣便在於此。通過閱讀 RxAndroid 原始碼和本文,你將獲得:
- RxJava、RxAndroid 和 Android 的連線
- Rx 世界裡鉤子的實現套路
- 高覆蓋率的單元測試
- Robolectric 對主執行緒的操縱
- 使用
CountDownLatch
來測試執行緒
(二)RxAndroid 簡介
RxJava 中執行緒的變換和函數語言程式設計與 Android 相得益彰,但是 RxJava 並非為 Android 量身打造。線上程變換的過程中,Android 有獨特的 UI 主執行緒的概念,因此,需要一個框架來連線 Android 和 RxJava。RxAndroid充當了該角色。
所以很明確,RxAndroid 的使命在於提供 Android 主執行緒的變換,程式碼如下:
Observable.just("one", "two", "three", "four", "five")
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(/* an Observer */);複製程式碼
其中, AndroidSchedulers.mainThread()
便是這個框架提供的能力。
說起 Android 主執行緒通訊問題,必不可少的關聯到 Handler、Looper、Message、HandlerThread等(可以檢視筆者的另一系列文章《Handler 和他的小夥伴們》)。
因此 RxAndroid 提供了更通用的能力,可以指定任意的 Looper 來進行任意執行緒之間的通訊:public static Scheduler from(Looper looper)
,程式碼舉例如下:
mHandlerThread = new HandlerThread("HandlerThread");
mHandlerThread.start();
Observable.just("one", "two", "three", "four", "five")
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.from(mHandlerThread.getLooper())
.subscribe(/* an Observer */);複製程式碼
本文基於 RxJava 和 RxAndroid 2.0.1 進行原始碼分析,Github地址如下:
(三)原始碼概覽
RxAndroid 非常簡潔,只有四個類,為了增加趣味性,對於其中的三個核心類,筆者稱之為面子、裡子和鉤子。
面子和裡子,互為表裡,面子是外在,靠裡子支撐;裡子是內涵,靠面子表現。
關於面子和裡子,軟體世界裡有個更專業的稱呼,叫門面模式。表裡如一,是值得尊敬的品格,現實世界如此,軟體世界也是如此,而 RxAndroid 更是如此。
以下是這個框架的精華部分:
- AndroidSchedulers:面子,即框架的門面,它的作用在簡介部分已經說明。
- HandlerScheduler:裡子,這個類框架的核心,由它處理與 Android 主執行緒通訊的邏輯。
- RxAndroidPlugins:鉤子,提供了行為的擴充套件。
- MainThreadDisposable:這個類是抽象類,提供了資源釋放的生命週期供重寫,它的作用是確保
onDispose()
在主執行緒執行,這個類的解析非本文的重點。 - 大量的單元測試:UT 同樣是框架的精華部分,這個框架 UT 非常完善,值得學習。
(四)面子 —— AndroidSchedulers
在上文的簡介中,我們已經領略到 AndroidSchedulers 作為門面的簡潔,它僅對外暴露了兩個方法。這裡重點分析一下 AndroidSchedulers.mainThread()
。
在此之前,我們先看下命名。RxJava 關於執行緒的取值,同樣也有個門面類,相關的程式碼如下:
- Schedulers.io()
- Schedulers.computation()
- Schedulers.newThread()
- Schedulers.single()
Schedulers
和 AndroidSchedulers
,以及 mainThread()
和上述方法在命名和實現上保持了高度的一致性。
接下來回到原始碼解析。mainThread
的實現並不複雜,但由於埋伏了兩個鉤子(RxAndroidPlugins),程式碼便顯得莫名其妙了,所以我們先忽視所有關於鉤子的邏輯,將程式碼精簡如下:
public static Scheduler mainThread() {
return new HandlerScheduler(new Handler(Looper.getMainLooper()));
}複製程式碼
如此一來,這個門面類的邏輯就十分簡單了,同時也呈現出了這個框架的裡子——HandlerScheduler,並且持有了主執行緒的 Looper 物件。
(五)裡子 —— HandlerScheduler
見名知意,這是一個與 Handler 有關的 Scheduler。與上文一樣,我們先來討論下命名的事情。在 RxJava 中,提供的預設執行緒我們都可以找到對應的實現,分別是:SingleScheduler、ComputationScheduler、IoScheduler 和 NewThreadScheduler,因此,這仍然是一個固定套路。
以上所有的 XxxxxScheduler
都有個共同的抽象父類,程式碼精簡如下,
public abstract class Scheduler {
public abstract Worker createWorker();
public abstract static class Worker implements Disposable {
public abstract Disposable schedule(Runnable run, long delay, TimeUnit unit);
}
}複製程式碼
因此 HandlerScheduler 只要根據父類的規範,做相應的抽象方法實現即可,其中 Worker.schedule()
中的邏輯將會在主執行緒執行。由於涉及到 Android 主執行緒通訊,在方法的實現中將使用到 Handler 機制。所以我們先來簡單回顧下 Handler 怎麼進行主執行緒的通訊。
當我們需要與主執行緒通訊時,發起的最終實現都是一致的:
handler.sendMessageAtTime(Message msg, long uptimeMillis)複製程式碼
那麼主執行緒如何處理訊息呢?共有兩種方式:
- 重寫 Handler 的 handleMessage()
- 為 Message 指定 callback 屬性(Runnable 型別),訊息發出後,callback將會在主執行緒中回撥。
以上兩種方式,通過閱讀 Handler 的 dispatchMessage()
原始碼可獲知:
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}複製程式碼
HandlerScheduler 使用了第2種方式與主執行緒通訊,原始碼精簡如下:
@Override
public Worker createWorker() {
return new HandlerWorker(handler);
}
private static final class HandlerWorker extends Worker {
//省略部分程式碼
@Override
public Disposable schedule(Runnable run, long delay, TimeUnit unit) {
// 對run物件進行代理,增加異常處理和釋放資源的邏輯
ScheduledRunnable scheduled = new ScheduledRunnable(handler, run);
// 內部將執行message.callback = scheduled
Message message = Message.obtain(handler, scheduled);
message.obj = this;
handler.sendMessageDelayed(message, Math.max(0L, unit.toMillis(delay)));
return scheduled;
}
}複製程式碼
行文至此,還有一個很重要的問題未解決: Message 物件的回撥函式 callback(Runnable 型別)的具體實現是什麼?
通過程式碼 debug 可以輕易的獲取到答案,但在此之前,我們先大膽預測一下:由於主執行緒執行的是 Observer
例項中的 onNext()
、onCompleted()
和 onError()
,因此 Runnable 便是由 Observer
例項封裝而來,並且在合適的時機執行上述三個方法。
事實如我們預測的一樣,Runnable 的具體實現為 ObservableObserveOn
類中的 ObserveOnObserver
內部類物件,它是 Runnable 的實現類。
HandlerScheduler
實現了 RxJava 、RxAndroid 和 Android 之間的連線。
(六)鉤子 —— RxAndroidPlugins
何為鉤子
首先解釋下鉤子的概念。一面牆壁光滑明亮,自然是賞心悅目,但是具備的功能性就弱了些,於是我們在牆壁上安置了一些鉤子,通過鉤子,我們可以掛上一些性感衣物或者一副世界名畫,這面牆壁的功能性便大大增強了。
牆壁便是 RxAndroid,並且內建了不少的鉤子,找到這些鉤子後,可以做很多擴充套件的事情,比如輸出日誌、異常處理和單元測試的輔助等。
縱觀 RxJava 和 RxAndroid 原始碼,埋伏了大量的鉤子,這也是造成一些原始碼閱讀起來比較費解的原因所在。所有的鉤子的讀寫的邏輯都內聚在 Plugins 中,RxJava 中是 RxJavaPlugins
,RxAndroid 中則是 RxAndroidPlugins
,同樣也保持了命名的一致性,而兩個 Plugins 類,也可以認為是所有鉤子的門面類。
發現鉤子
在講面子這一節的時候,筆者對 AndroidSchedulers.mainThread()
原始碼做了精簡,實際上這裡埋伏了個鉤子 onMainThreadHandler
:
//MAIN_THREAD 同樣埋伏了鉤子,此處不做介紹,最終將返回HandlerScheduler物件
public static Scheduler mainThread() {
return RxAndroidPlugins.onMainThreadScheduler(MAIN_THREAD);
}
// 以下為 RxAndroidPlugins.java
public static Scheduler onMainThreadScheduler(Scheduler scheduler) {
// 鉤子:onMainThreadHandler
Function<Scheduler, Scheduler> f = onMainThreadHandler;
if (f == null) {
return scheduler;
}
return apply(f, scheduler);
}
// 為鉤子賦值
public static void setMainThreadSchedulerHandler(Function<Scheduler, Scheduler> handler) {
onMainThreadHandler = handler;
}複製程式碼
為了加深理解,可以對照下面的流程圖檢視,大部分的鉤子都是基於同樣的套路來實現的。
RxAndroid 和 RxJava 2.x 內建了大量的鉤子,而他們都以 get
和 set
的形式對外部提供讀寫。如下圖:
對於鉤子,我們需要一些具體的例項來加深理解,並且希望從框架原始碼本身來尋找例項,此時,單元測試將大展身手。
(七)單元測試是框架最好的說明書
鉤子的 UT 解讀
結合上一節,我們來解析下這個框架的單元測試,挖掘原始碼本身的更多資訊量。
針對鉤子的邏輯,我們一起來看下其中的一個測試方法:AndroidSchedulersTest
中的 mainThreadCallsThroughToHook()
,方法名其實已經表明的 UT 的測試意圖,即對該場景進行測試:通過鉤子(Hook)來執行 AndroidSchedulers.mainThread()
方法。
這個例子告訴我們:
- 如何為鉤子賦值,並定義擴充套件的行為
- 鉤子中擴充套件的行為觸發的時機
所以說,單元測試是框架最好的說明書。
高覆蓋率
通過 AS 的 Run Tests with Coverage,資料表明:這個框架的單元測試行覆蓋率達到 91%,如下圖:
Robolectric 對主執行緒的操縱
RxAndroid 使用 Robolectric 對 Android 相關的邏輯進行測試。通過 ShadowLooper
可以操縱主執行緒,如下圖所示,此 UT 位於 HandlerSchedulerTest
的 directScheduleOnceWithDelayPostsWithDelay()
。
這個例子告訴我們:
ShadowLooper.runUiThreadTasks()
可以模擬主執行緒執行ShadowLooper.idleMainLooper()
可以指定時間來阻塞主執行緒- 除此之外
ShadowLooper
還提供了很多好用的 api 來操縱主執行緒,可以通過 ShadowLooper 的原始碼去了解這些 api 的用途。
另外,MainThreadDisposableTest
中的 UT 向我們展示瞭如何使用 CountDownLatch
來測試執行緒,有興趣的同學可以閱讀這部分原始碼。
總而言之,一個優秀的框架中的單元測試,既能幫助我們更好的瞭解框架本身,也能幫助我們提高單元測試的技巧。
(八)總結
面子、裡子和鉤子組成了 RxAndroid 的全部,而單元測試在此基礎上起到了錦上添花的作用,這依然是一個麻雀雖小五臟俱全的優秀開源框架,每一個優秀的框架,都是一本書一部電影,值得反覆揣摩,用心研究。