Rxjava這一篇就夠了,牆裂推薦

Scus發表於2019-03-02

最近專案裡面有用到Rxjava框架,感覺很強大的鉅作,所以在網上搜了很多相關文章,發現一片文章很不錯,今天把這篇文章分享給大家,感謝原作者的傾心貢獻,那麼下面大家來看看這篇文章:

前言

我從去年開始使用 RxJava ,到現在一年多了。今年加入了 Flipboard 後,看到 Flipboard 的 Android 專案也在使用 RxJava ,並且使用的場景越來越多 。而最近這幾個月,我也發現國內越來越多的人開始提及 RxJava 。有人說『RxJava 真是太好用了』,有人說『RxJava 真是太難用了』,另外更多的人表示:我真的百度了也谷歌了,但我還是想問: RxJava 到底是什麼?

鑑於 RxJava 目前這種既火爆又神祕的現狀,而我又在一年的使用過程中對 RxJava 有了一些理解,我決定寫下這篇文章來對 RxJava 做一個相對詳細的、針對 Android 開發者的介紹。

這篇文章的目的有兩個: 1. 給對 RxJava 感興趣的人一些入門的指引 2. 給正在使用 RxJava 但仍然心存疑惑的人一些更深入的解析

目錄:

在正文開始之前的最後,放上 GitHub 連結和引入依賴的 gradle 程式碼: Github:
https://github.com/ReactiveX/RxJava
https://github.com/ReactiveX/RxAndroid
引入依賴:
compile `io.reactivex:rxjava:1.0.14`
compile `io.reactivex:rxandroid:1.0.1`
(版本號是文章釋出時的最新穩定版)

另外,感謝 RxJava 核心成員流火楓林的技術支援和內測讀者程式碼家鮑永章drakeet馬琳有時放縱程式亦非猿大頭鬼XZoomEye席德雨TCaheadTiiimeAilurus宅學長妖孽大大大大大臣哥NicodeLee的幫助,以及周伯通招聘的贊助。

RxJava 到底是什麼

一個詞:非同步

RxJava 在 GitHub 主頁上的自我介紹是 “a library for composing asynchronous and event-based programs using observable sequences for the Java VM”(一個在 Java VM 上使用可觀測的序列來組成非同步的、基於事件的程式的庫)。這就是 RxJava ,概括得非常精準。

然而,對於初學者來說,這太難看懂了。因為它是一個『總結』,而初學者更需要一個『引言』。

其實, RxJava 的本質可以壓縮為非同步這一個詞。說到根上,它就是一個實現非同步操作的庫,而別的定語都是基於這之上的。

RxJava 好在哪

換句話說,『同樣是做非同步,為什麼人們用它,而不用現成的 AsyncTask / Handler / XXX / … ?』

一個詞:簡潔

非同步操作很關鍵的一點是程式的簡潔性,因為在排程過程比較複雜的情況下,非同步程式碼經常會既難寫也難被讀懂。 Android 創造的 AsyncTaskHandler ,其實都是為了讓非同步程式碼更加簡潔。RxJava 的優勢也是簡潔,但它的簡潔的與眾不同之處在於,隨著程式邏輯變得越來越複雜,它依然能夠保持簡潔。

舉個例子

假設有這樣一個需求:介面上有一個自定義的檢視 imageCollectorView ,它的作用是顯示多張圖片,並能使用 addImage(Bitmap) 方法來任意增加顯示的圖片。現在需要程式將一個給出的目錄陣列 File[] folders 中每個目錄下的 png 圖片都載入出來並顯示在 imageCollectorView 中。需要注意的是,由於讀取圖片的這一過程較為耗時,需要放在後臺執行,而圖片的顯示則必須在 UI 執行緒執行。常用的實現方式有多種,我這裡貼出其中一種:

new Thread() {
    @Override
    public void run() {
        super.run();
        for (File folder : folders) {
            File[] files = folder.listFiles();
            for (File file : files) {
                if (file.getName().endsWith(".png")) {
                    final Bitmap bitmap = getBitmapFromFile(file);
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            imageCollectorView.addImage(bitmap);
                        }
                    });
                }
            }
        }
    }
}.start();複製程式碼

而如果使用 RxJava ,實現方式是這樣的:

Observable.from(folders)
    .flatMap(new Func1<File, Observable<File>>() {
        @Override
        public Observable<File> call(File file) {
            return Observable.from(file.listFiles());
        }
    })
    .filter(new Func1<File, Boolean>() {
        @Override
        public Boolean call(File file) {
            return file.getName().endsWith(".png");
        }
    })
    .map(new Func1<File, Bitmap>() {
        @Override
        public Bitmap call(File file) {
            return getBitmapFromFile(file);
        }
    })
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Bitmap>() {
        @Override
        public void call(Bitmap bitmap) {
            imageCollectorView.addImage(bitmap);
        }
    });複製程式碼

那位說話了:『你這程式碼明明變多了啊!簡潔個毛啊!』大兄弟你消消氣,我說的是邏輯的簡潔,不是單純的程式碼量少(邏輯簡潔才是提升讀寫程式碼速度的必殺技對不?)。觀察一下你會發現, RxJava 的這個實現,是一條從上到下的鏈式呼叫,沒有任何巢狀,這在邏輯的簡潔性上是具有優勢的。當需求變得複雜時,這種優勢將更加明顯(試想如果還要求只選取前 10 張圖片,常規方式要怎麼辦?如果有更多這樣那樣的要求呢?再試想,在這一大堆需求實現完兩個月之後需要改功能,當你翻回這裡看到自己當初寫下的那一片迷之縮排,你能保證自己將迅速看懂,而不是對著程式碼重新捋一遍思路?)。

另外,如果你的 IDE 是 Android Studio ,其實每次開啟某個 Java 檔案的時候,你會看到被自動 Lambda 化的預覽,這將讓你更加清晰地看到程式邏輯:

Observable.from(folders)
    .flatMap((Func1) (folder) -> { Observable.from(file.listFiles()) })
    .filter((Func1) (file) -> { file.getName().endsWith(".png") })
    .map((Func1) (file) -> { getBitmapFromFile(file) })
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe((Action1) (bitmap) -> { imageCollectorView.addImage(bitmap) });複製程式碼

如果你習慣使用 Retrolambda ,你也可以直接把程式碼寫成上面這種簡潔的形式。而如果你看到這裡還不知道什麼是 Retrolambda ,我不建議你現在就去學習它。原因有兩點:1. Lambda 是把雙刃劍,它讓你的程式碼簡潔的同時,降低了程式碼的可讀性,因此同時學習 RxJava 和 Retrolambda 可能會讓你忽略 RxJava 的一些技術細節;2. Retrolambda 是 Java 6/7 對 Lambda 表示式的非官方相容方案,它的向後相容性和穩定性是無法保障的,因此對於企業專案,使用 Retrolambda 是有風險的。所以,與很多 RxJava 的推廣者不同,我並不推薦在學習 RxJava 的同時一起學習 Retrolambda。事實上,我個人雖然很欣賞 Retrolambda,但我從來不用它。

在Flipboard 的 Android 程式碼中,有一段邏輯非常複雜,包含了多次記憶體操作、本地檔案操作和網路操作,物件分分合合,執行緒間相互配合相互等待,一會兒排成人字,一會兒排成一字。如果使用常規的方法來實現,肯定是要寫得欲仙欲死,然而在使用 RxJava 的情況下,依然只是一條鏈式呼叫就完成了。它很長,但很清晰。

所以, RxJava 好在哪?就好在簡潔,好在那把什麼複雜邏輯都能穿成一條線的簡潔。

API 介紹和原理簡析

這個我就做不到一個詞說明了……因為這一節的主要內容就是一步步地說明 RxJava 到底怎樣做到了非同步,怎樣做到了簡潔。

1. 概念:擴充套件的觀察者模式

RxJava 的非同步實現,是通過一種擴充套件的觀察者模式來實現的。

觀察者模式

先簡述一下觀察者模式,已經熟悉的可以跳過這一段。

觀察者模式面向的需求是:A 物件(觀察者)對 B 物件(被觀察者)的某種變化高度敏感,需要在 B 變化的一瞬間做出反應。舉個例子,新聞裡喜聞樂見的警察抓小偷,警察需要在小偷伸手作案的時候實施抓捕。在這個例子裡,警察是觀察者,小偷是被觀察者,警察需要時刻盯著小偷的一舉一動,才能保證不會漏過任何瞬間。程式的觀察者模式和這種真正的『觀察』略有不同,觀察者不需要時刻盯著被觀察者(例如 A 不需要每過 2ms 就檢查一次 B 的狀態),而是採用註冊(Register)或者稱為訂閱(Subscribe)的方式,告訴被觀察者:我需要你的某某狀態,你要在它變化的時候通知我。 Android 開發中一個比較典型的例子是點選監聽器 OnClickListener 。對設定 OnClickListener 來說, View 是被觀察者, OnClickListener 是觀察者,二者通過 setOnClickListener() 方法達成訂閱關係。訂閱之後使用者點選按鈕的瞬間,Android Framework 就會將點選事件傳送給已經註冊的 OnClickListener 。採取這樣被動的觀察方式,既省去了反覆檢索狀態的資源消耗,也能夠得到最高的反饋速度。當然,這也得益於我們可以隨意定製自己程式中的觀察者和被觀察者,而警察叔叔明顯無法要求小偷『你在作案的時候務必通知我』。

OnClickListener 的模式大致如下圖:

OnClickListener 觀察者模式

如圖所示,通過 setOnClickListener() 方法,Button 持有 OnClickListener 的引用(這一過程沒有在圖上畫出);當使用者點選時,Button 自動呼叫 OnClickListeneronClick() 方法。另外,如果把這張圖中的概念抽象出來(Button -> 被觀察者、OnClickListener -> 觀察者、setOnClickListener() -> 訂閱,onClick() -> 事件),就由專用的觀察者模式(例如只用於監聽控制元件點選)轉變成了通用的觀察者模式。如下圖:

通用觀察者模式

而 RxJava 作為一個工具庫,使用的就是通用形式的觀察者模式。

RxJava 的觀察者模式

RxJava 有四個基本概念:Observable (可觀察者,即被觀察者)、 Observer (觀察者)、 subscribe (訂閱)、事件。ObservableObserver 通過 subscribe() 方法實現訂閱關係,從而 Observable 可以在需要的時候發出事件來通知 Observer

與傳統觀察者模式不同, RxJava 的事件回撥方法除了普通事件 onNext() (相當於 onClick() / onEvent())之外,還定義了兩個特殊的事件:onCompleted()onError()

  • onCompleted(): 事件佇列完結。RxJava 不僅把每個事件單獨處理,還會把它們看做一個佇列。RxJava 規定,當不會再有新的 onNext() 發出時,需要觸發 onCompleted() 方法作為標誌。
  • onError(): 事件佇列異常。在事件處理過程中出異常時,onError() 會被觸發,同時佇列自動終止,不允許再有事件發出。
  • 在一個正確執行的事件序列中, onCompleted()onError() 有且只有一個,並且是事件序列中的最後一個。需要注意的是,onCompleted()onError() 二者也是互斥的,即在佇列中呼叫了其中一個,就不應該再呼叫另一個。

RxJava 的觀察者模式大致如下圖:

RxJava 的觀察者模式

2. 基本實現

基於以上的概念, RxJava 的基本實現主要有三點:

1) 建立 Observer

Observer 即觀察者,它決定事件觸發的時候將有怎樣的行為。 RxJava 中的 Observer 介面的實現方式:

Observer<String> observer = new Observer<String>() {
    @Override
    public void onNext(String s) {
        Log.d(tag, "Item: " + s);
    }

    @Override
    public void onCompleted() {
        Log.d(tag, "Completed!");
    }

    @Override
    public void onError(Throwable e) {
        Log.d(tag, "Error!");
    }
};複製程式碼

除了 Observer 介面之外,RxJava 還內建了一個實現了 Observer 的抽象類:SubscriberSubscriberObserver 介面進行了一些擴充套件,但他們的基本使用方式是完全一樣的:

Subscriber<String> subscriber = new Subscriber<String>() {
    @Override
    public void onNext(String s) {
        Log.d(tag, "Item: " + s);
    }

    @Override
    public void onCompleted() {
        Log.d(tag, "Completed!");
    }

    @Override
    public void onError(Throwable e) {
        Log.d(tag, "Error!");
    }
};複製程式碼

不僅基本使用方式一樣,實質上,在 RxJava 的 subscribe 過程中,Observer 也總是會先被轉換成一個 Subscriber 再使用。所以如果你只想使用基本功能,選擇 ObserverSubscriber 是完全一樣的。它們的區別對於使用者來說主要有兩點:

  1. onStart(): 這是 Subscriber 增加的方法。它會在 subscribe 剛開始,而事件還未傳送之前被呼叫,可以用於做一些準備工作,例如資料的清零或重置。這是一個可選方法,預設情況下它的實現為空。需要注意的是,如果對準備工作的執行緒有要求(例如彈出一個顯示進度的對話方塊,這必須在主執行緒執行), onStart() 就不適用了,因為它總是在 subscribe 所發生的執行緒被呼叫,而不能指定執行緒。要在指定的執行緒來做準備工作,可以使用 doOnSubscribe() 方法,具體可以在後面的文中看到。
  2. unsubscribe(): 這是 Subscriber 所實現的另一個介面 Subscription 的方法,用於取消訂閱。在這個方法被呼叫後,Subscriber 將不再接收事件。一般在這個方法呼叫前,可以使用 isUnsubscribed() 先判斷一下狀態。 unsubscribe() 這個方法很重要,因為在 subscribe() 之後, Observable 會持有 Subscriber 的引用,這個引用如果不能及時被釋放,將有記憶體洩露的風險。所以最好保持一個原則:要在不再使用的時候儘快在合適的地方(例如 onPause() onStop() 等方法中)呼叫 unsubscribe() 來解除引用關係,以避免記憶體洩露的發生。
2) 建立 Observable

Observable 即被觀察者,它決定什麼時候觸發事件以及觸發怎樣的事件。 RxJava 使用 create() 方法來建立一個 Observable ,併為它定義事件觸發規則:

Observable observable = Observable.create(new Observable.OnSubscribe<String>() {
    @Override
    public void call(Subscriber<? super String> subscriber) {
        subscriber.onNext("Hello");
        subscriber.onNext("Hi");
        subscriber.onNext("Aloha");
        subscriber.onCompleted();
    }
});複製程式碼

可以看到,這裡傳入了一個 OnSubscribe 物件作為引數。OnSubscribe 會被儲存在返回的 Observable 物件中,它的作用相當於一個計劃表,當 Observable 被訂閱的時候,OnSubscribecall() 方法會自動被呼叫,事件序列就會依照設定依次觸發(對於上面的程式碼,就是觀察者Subscriber 將會被呼叫三次 onNext() 和一次 onCompleted())。這樣,由被觀察者呼叫了觀察者的回撥方法,就實現了由被觀察者向觀察者的事件傳遞,即觀察者模式。

這個例子很簡單:事件的內容是字串,而不是一些複雜的物件;事件的內容是已經定好了的,而不像有的觀察者模式一樣是待確定的(例如網路請求的結果在請求返回之前是未知的);所有事件在一瞬間被全部傳送出去,而不是夾雜一些確定或不確定的時間間隔或者經過某種觸發器來觸發的。總之,這個例子看起來毫無實用價值。但這是為了便於說明,實質上只要你想,各種各樣的事件傳送規則你都可以自己來寫。至於具體怎麼做,後面都會講到,但現在不行。只有把基礎原理先說明白了,上層的運用才能更容易說清楚。

create() 方法是 RxJava 最基本的創造事件序列的方法。基於這個方法, RxJava 還提供了一些方法用來快捷建立事件佇列,例如:

  • just(T...): 將傳入的引數依次傳送出來。
Observable observable = Observable.just("Hello", "Hi", "Aloha");
// 將會依次呼叫:
// onNext("Hello");
// onNext("Hi");
// onNext("Aloha");
// onCompleted();複製程式碼
  • from(T[]) / from(Iterable<? extends T>) : 將傳入的陣列或 Iterable 拆分成具體物件後,依次傳送出來。
String[] words = {"Hello", "Hi", "Aloha"};
Observable observable = Observable.from(words);
// 將會依次呼叫:
// onNext("Hello");
// onNext("Hi");
// onNext("Aloha");
// onCompleted();複製程式碼

上面 just(T...) 的例子和 from(T[]) 的例子,都和之前的 create(OnSubscribe) 的例子是等價的。

3) Subscribe (訂閱)

建立了 ObservableObserver 之後,再用 subscribe() 方法將它們聯結起來,整條鏈子就可以工作了。程式碼形式很簡單:

observable.subscribe(observer);
// 或者:
observable.subscribe(subscriber);複製程式碼

有人可能會注意到, subscribe() 這個方法有點怪:它看起來是『observalbe 訂閱了 observer / subscriber』而不是『observer / subscriber 訂閱了 observalbe』,這看起來就像『雜誌訂閱了讀者』一樣顛倒了物件關係。這讓人讀起來有點彆扭,不過如果把 API 設計成 observer.subscribe(observable) / subscriber.subscribe(observable) ,雖然更加符合思維邏輯,但對流式 API 的設計就造成影響了,比較起來明顯是得不償失的。

Observable.subscribe(Subscriber) 的內部實現是這樣的(僅核心程式碼):

// 注意:這不是 subscribe() 的原始碼,而是將原始碼中與效能、相容性、擴充套件性有關的程式碼剔除後的核心程式碼。
// 如果需要看原始碼,可以去 RxJava 的 GitHub 倉庫下載。
public Subscription subscribe(Subscriber subscriber) {
    subscriber.onStart();
    onSubscribe.call(subscriber);
    return subscriber;
}複製程式碼

可以看到,subscriber() 做了3件事:

  1. 呼叫 Subscriber.onStart() 。這個方法在前面已經介紹過,是一個可選的準備方法。
  2. 呼叫 Observable 中的 OnSubscribe.call(Subscriber) 。在這裡,事件傳送的邏輯開始執行。從這也可以看出,在 RxJava 中, Observable 並不是在建立的時候就立即開始傳送事件,而是在它被訂閱的時候,即當 subscribe() 方法執行的時候。
  3. 將傳入的 Subscriber 作為 Subscription 返回。這是為了方便 unsubscribe().

整個過程中物件間的關係如下圖:

關係靜圖

或者可以看動圖:

關係靜圖

除了 subscribe(Observer)subscribe(Subscriber)subscribe() 還支援不完整定義的回撥,RxJava 會自動根據定義建立出 Subscriber 。形式如下:

Action1<String> onNextAction = new Action1<String>() {
    // onNext()
    @Override
    public void call(String s) {
        Log.d(tag, s);
    }
};
Action1<Throwable> onErrorAction = new Action1<Throwable>() {
    // onError()
    @Override
    public void call(Throwable throwable) {
        // Error handling
    }
};
Action0 onCompletedAction = new Action0() {
    // onCompleted()
    @Override
    public void call() {
        Log.d(tag, "completed");
    }
};

// 自動建立 Subscriber ,並使用 onNextAction 來定義 onNext()
observable.subscribe(onNextAction);
// 自動建立 Subscriber ,並使用 onNextAction 和 onErrorAction 來定義 onNext() 和 onError()
observable.subscribe(onNextAction, onErrorAction);
// 自動建立 Subscriber ,並使用 onNextAction、 onErrorAction 和 onCompletedAction 來定義 onNext()、 onError() 和 onCompleted()
observable.subscribe(onNextAction, onErrorAction, onCompletedAction);複製程式碼

簡單解釋一下這段程式碼中出現的 Action1Action0Action0 是 RxJava 的一個介面,它只有一個方法 call(),這個方法是無參無返回值的;由於 onCompleted() 方法也是無參無返回值的,因此 Action0 可以被當成一個包裝物件,將 onCompleted() 的內容打包起來將自己作為一個引數傳入 subscribe() 以實現不完整定義的回撥。這樣其實也可以看做將 onCompleted() 方法作為引數傳進了 subscribe(),相當於其他某些語言中的『閉包』。 Action1 也是一個介面,它同樣只有一個方法 call(T param),這個方法也無返回值,但有一個引數;與 Action0 同理,由於 onNext(T obj)onError(Throwable error) 也是單引數無返回值的,因此 Action1 可以將 onNext(obj)onError(error) 打包起來傳入 subscribe() 以實現不完整定義的回撥。事實上,雖然 Action0Action1 在 API 中使用最廣泛,但 RxJava 是提供了多個 ActionX 形式的介面 (例如 Action2, Action3) 的,它們可以被用以包裝不同的無返回值的方法。

注:正如前面所提到的,ObserverSubscriber 具有相同的角色,而且 Observersubscribe() 過程中最終會被轉換成 Subscriber 物件,因此,從這裡開始,後面的描述我將用 Subscriber 來代替 Observer ,這樣更加嚴謹。

4) 場景示例

下面舉兩個例子:

為了把原理用更清晰的方式表述出來,本文中挑選的都是功能儘可能簡單的例子,以至於有些示例程式碼看起來會有『畫蛇添足』『明明不用 RxJava 可以更簡便地解決問題』的感覺。當你看到這種情況,不要覺得是因為 RxJava 太囉嗦,而是因為在過早的時候舉出真實場景的例子並不利於原理的解析,因此我刻意挑選了簡單的情景。

a. 列印字串陣列

將字串陣列 names 中的所有字串依次列印出來:

String[] names = ...;
Observable.from(names)
    .subscribe(new Action1<String>() {
        @Override
        public void call(String name) {
            Log.d(tag, name);
        }
    });複製程式碼
b. 由 id 取得圖片並顯示

由指定的一個 drawable 檔案 id drawableRes 取得圖片,並顯示在 ImageView 中,並在出現異常的時候列印 Toast 報錯:

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();
    }
}).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();
    }
});複製程式碼

正如上面兩個例子這樣,建立出 ObservableSubscriber ,再用 subscribe() 將它們串起來,一次 RxJava 的基本使用就完成了。非常簡單。

然而,

這並沒有什麼diao用

在 RxJava 的預設規則中,事件的發出和消費都是在同一個執行緒的。也就是說,如果只用上面的方法,實現出來的只是一個同步的觀察者模式。觀察者模式本身的目的就是『後臺處理,前臺回撥』的非同步機制,因此非同步對於 RxJava 是至關重要的。而要實現非同步,則需要用到 RxJava 的另一個概念: Scheduler

3. 執行緒控制 —— Scheduler (一)

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

1) Scheduler 的 API (一)

在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。
  • 另外, Android 還有一個專用的 AndroidSchedulers.mainThread(),它指定的操作將在 Android 主執行緒執行。

有了這幾個 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 數字的列印將發生在主執行緒 。事實上,這種在 subscribe() 之前寫上兩句 subscribeOn(Scheduler.io())observeOn(AndroidSchedulers.mainThread()) 的使用方式非常常見,它適用於多數的 『後臺執行緒取資料,主執行緒顯示』的程式策略。

而前面提到的由圖片 id 取得圖片並顯示的例子,如果也加上這兩句:

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();
    }
})
.subscribeOn(Schedulers.io()) // 指定 subscribe() 發生在 IO 執行緒
.observeOn(AndroidSchedulers.mainThread()) // 指定 Subscriber 的回撥發生在主執行緒
.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) Scheduler 的原理 (一)

RxJava 的 Scheduler API 很方便,也很神奇(加了一句話就把執行緒切換了,怎麼做到的?而且 subscribe() 不是最外層直接呼叫的方法嗎,它竟然也能被指定執行緒?)。然而 Scheduler 的原理需要放在後面講,因為它的原理是以下一節《變換》的原理作為基礎的。

好吧這一節其實我屁也沒說,只是為了讓你安心,讓你知道我不是忘了講原理,而是把它放在了更合適的地方。

4. 變換

終於要到牛逼的地方了,不管你激動不激動,反正我是激動了。

RxJava 提供了對事件序列進行變換的支援,這是它的核心功能之一,也是大多數人說『RxJava 真是太好用了』的最大原因。所謂變換,就是將事件序列中的物件或整個序列進行加工處理,轉換成不同的事件或事件序列。概念說著總是模糊難懂的,來看 API。

1) API

首先看一個 map() 的例子:

Observable.just("images/logo.png") // 輸入型別 String
    .map(new Func1<String, Bitmap>() {
        @Override
        public Bitmap call(String filePath) { // 引數型別 String
            return getBitmapFromPath(filePath); // 返回型別 Bitmap
        }
    })
    .subscribe(new Action1<Bitmap>() {
        @Override
        public void call(Bitmap bitmap) { // 引數型別 Bitmap
            showBitmap(bitmap);
        }
    });複製程式碼

這裡出現了一個叫做 Func1 的類。它和 Action1 非常相似,也是 RxJava 的一個介面,用於包裝含有一個引數的方法。 Func1Action的區別在於, Func1 包裝的是有返回值的方法。另外,和 ActionX 一樣, FuncX 也有多個,用於不同引數個數的方法。FuncXActionX 的區別在 FuncX 包裝的是有返回值的方法。

可以看到,map() 方法將引數中的 String 物件轉換成一個 Bitmap 物件後返回,而在經過 map() 方法後,事件的引數型別也由 String轉為了 Bitmap。這種直接變換物件並返回的,是最常見的也最容易理解的變換。不過 RxJava 的變換遠不止這樣,它不僅可以針對事件物件,還可以針對整個事件佇列,這使得 RxJava 變得非常靈活。我列舉幾個常用的變換:

  • map(): 事件物件的直接變換,具體功能上面已經介紹過。它是 RxJava 最常用的變換。 map() 的示意圖:map() 示意圖

  • flatMap(): 這是一個很有用但非常難理解的變換,因此我決定花多些篇幅來介紹它。 首先假設這麼一種需求:假設有一個資料結構『學生』,現在需要列印出一組學生的名字。實現方式很簡單:

Student[] students = ...;
Subscriber<String> subscriber = new Subscriber<String>() {
    @Override
    public void onNext(String name) {
        Log.d(tag, name);
    }
    ...
};
Observable.from(students)
    .map(new Func1<Student, String>() {
        @Override
        public String call(Student student) {
            return student.getName();
        }
    })
    .subscribe(subscriber);複製程式碼

很簡單。那麼再假設:如果要列印出每個學生所需要修的所有課程的名稱呢?(需求的區別在於,每個學生只有一個名字,但卻有多個課程。)首先可以這樣實現:

Student[] students = ...;
Subscriber<Student> subscriber = new Subscriber<Student>() {
    @Override
    public void onNext(Student student) {
        List<Course> courses = student.getCourses();
        for (int i = 0; i < courses.size(); i++) {
            Course course = courses.get(i);
            Log.d(tag, course.getName());
        }
    }
    ...
};
Observable.from(students)
    .subscribe(subscriber);複製程式碼

依然很簡單。那麼如果我不想在 Subscriber 中使用 for 迴圈,而是希望 Subscriber 中直接傳入單個的 Course 物件呢(這對於程式碼複用很重要)?用 map() 顯然是不行的,因為 map() 是一對一的轉化,而我現在的要求是一對多的轉化。那怎麼才能把一個 Student 轉化成多個 Course 呢?

這個時候,就需要用 flatMap() 了:

Student[] students = ...;
Subscriber<Course> subscriber = new Subscriber<Course>() {
    @Override
    public void onNext(Course course) {
        Log.d(tag, course.getName());
    }
    ...
};
Observable.from(students)
    .flatMap(new Func1<Student, Observable<Course>>() {
        @Override
        public Observable<Course> call(Student student) {
            return Observable.from(student.getCourses());
        }
    })
    .subscribe(subscriber);複製程式碼

從上面的程式碼可以看出, flatMap()map() 有一個相同點:它也是把傳入的引數轉化之後返回另一個物件。但需要注意,和 map() 不同的是, flatMap() 中返回的是個 Observable 物件,並且這個 Observable 物件並不是被直接傳送到了 Subscriber 的回撥方法中。 flatMap() 的原理是這樣的:1. 使用傳入的事件物件建立一個 Observable 物件;2. 並不傳送這個 Observable, 而是將它啟用,於是它開始傳送事件;3. 每一個建立出來的 Observable 傳送的事件,都被匯入同一個 Observable ,而這個 Observable 負責將這些事件統一交給 Subscriber 的回撥方法。這三個步驟,把事件拆成了兩級,通過一組新建立的 Observable 將初始的物件『鋪平』之後通過統一路徑分發了下去。而這個『鋪平』就是 flatMap() 所謂的 flat。

flatMap() 示意圖:

flatMap() 示意圖

擴充套件:由於可以在巢狀的 Observable 中新增非同步程式碼, flatMap() 也常用於巢狀的非同步操作,例如巢狀的網路請求。示例程式碼(Retrofit + RxJava):

networkClient.token() // 返回 Observable<String>,在訂閱時請求 token,並在響應後傳送 token
    .flatMap(new Func1<String, Observable<Messages>>() {
        @Override
        public Observable<Messages> call(String token) {
            // 返回 Observable<Messages>,在訂閱時請求訊息列表,並在響應後傳送請求到的訊息列表
            return networkClient.messages();
        }
    })
    .subscribe(new Action1<Messages>() {
        @Override
        public void call(Messages messages) {
            // 處理顯示訊息列表
            showMessages(messages);
        }
    });複製程式碼

傳統的巢狀請求需要使用巢狀的 Callback 來實現。而通過 flatMap() ,可以把巢狀的請求寫在一條鏈中,從而保持程式邏輯的清晰。

  • throttleFirst(): 在每次事件觸發後的一定時間間隔內丟棄新的事件。常用作去抖動過濾,例如按鈕的點選監聽器:RxView.clickEvents(button) // RxBinding 程式碼,後面的文章有解釋 .throttleFirst(500, TimeUnit.MILLISECONDS) // 設定防抖間隔為 500ms .subscribe(subscriber);媽媽再也不怕我的使用者手抖點開兩個重複的介面啦。

此外, RxJava 還提供很多便捷的方法來實現事件序列的變換,這裡就不一一舉例了。

2) 變換的原理:lift()

這些變換雖然功能各有不同,但實質上都是針對事件序列的處理和再傳送。而在 RxJava 的內部,它們是基於同一個基礎的變換方法: lift(Operator)。首先看一下 lift() 的內部實現(僅核心程式碼):

// 注意:這不是 lift() 的原始碼,而是將原始碼中與效能、相容性、擴充套件性有關的程式碼剔除後的核心程式碼。
// 如果需要看原始碼,可以去 RxJava 的 GitHub 倉庫下載。
public <R> Observable<R> lift(Operator<? extends R, ? super T> operator) {
    return Observable.create(new OnSubscribe<R>() {
        @Override
        public void call(Subscriber subscriber) {
            Subscriber newSubscriber = operator.call(subscriber);
            newSubscriber.onStart();
            onSubscribe.call(newSubscriber);
        }
    });
}複製程式碼

這段程式碼很有意思:它生成了一個新的 Observable 並返回,而且建立新 Observable 所用的引數 OnSubscribe 的回撥方法 call() 中的實現竟然看起來和前面講過的 Observable.subscribe() 一樣!然而它們並不一樣喲~不一樣的地方關鍵就在於第二行 onSubscribe.call(subscriber) 中的 onSubscribe 所指代的物件不同(高能預警:接下來的幾句話可能會導致身體的嚴重不適)——

  • subscribe() 中這句話的 onSubscribe 指的是 Observable 中的 onSubscribe 物件,這個沒有問題,但是 lift() 之後的情況就複雜了點。
  • 當含有 lift() 時:
    1.lift() 建立了一個 Observable 後,加上之前的原始 Observable,已經有兩個 Observable 了;
    2.而同樣地,新 Observable 裡的新 OnSubscribe 加上之前的原始 Observable 中的原始 OnSubscribe,也就有了兩個 OnSubscribe
    3.當使用者呼叫經過 lift() 後的 Observablesubscribe() 的時候,使用的是 lift() 所返回的新的 Observable ,於是它所觸發的 onSubscribe.call(subscriber),也是用的新 Observable 中的新 OnSubscribe,即在 lift() 中生成的那個 OnSubscribe
    4.而這個新 OnSubscribecall() 方法中的 onSubscribe ,就是指的原始 Observable 中的原始 OnSubscribe ,在這個 call() 方法裡,新 OnSubscribe 利用 operator.call(subscriber) 生成了一個新的 SubscriberOperator 就是在這裡,通過自己的 call() 方法將新 Subscriber 和原始 Subscriber 進行關聯,並插入自己的『變換』程式碼以實現變換),然後利用這個新 Subscriber 向原始 Observable 進行訂閱。
    這樣就實現了 lift() 過程,有點像一種代理機制,通過事件攔截和處理實現事件序列的變換。

精簡掉細節的話,也可以這麼說:在 Observable 執行了 lift(Operator) 方法之後,會返回一個新的 Observable,這個新的 Observable 會像一個代理一樣,負責接收原始的 Observable 發出的事件,並在處理後傳送給 Subscriber

如果你更喜歡具象思維,可以看圖:

lift() 原理圖

或者可以看動圖:

lift 原理動圖

兩次和多次的 lift() 同理,如下圖:

兩次 lift

舉一個具體的 Operator 的實現。下面這是一個將事件中的 Integer 物件轉換成 String 的例子,僅供參考:

observable.lift(new Observable.Operator<String, Integer>() {
    @Override
    public Subscriber<? super Integer> call(final Subscriber<? super String> subscriber) {
        // 將事件序列中的 Integer 物件轉換為 String 物件
        return new Subscriber<Integer>() {
            @Override
            public void onNext(Integer integer) {
                subscriber.onNext("" + integer);
            }

            @Override
            public void onCompleted() {
                subscriber.onCompleted();
            }

            @Override
            public void onError(Throwable e) {
                subscriber.onError(e);
            }
        };
    }
});複製程式碼

講述 lift() 的原理只是為了讓你更好地瞭解 RxJava ,從而可以更好地使用它。然而不管你是否理解了 lift() 的原理,RxJava 都不建議開發者自定義 Operator 來直接使用 lift(),而是建議儘量使用已有的 lift() 包裝方法(如 map() flatMap() 等)進行組合來實現需求,因為直接使用 lift() 非常容易發生一些難以發現的錯誤。

3) compose: 對 Observable 整體的變換

除了 lift() 之外, Observable 還有一個變換方法叫做 compose(Transformer)。它和 lift() 的區別在於, lift() 是針對事件項和事件序列的,而 compose() 是針對 Observable 自身進行變換。舉個例子,假設在程式中有多個 Observable ,並且他們都需要應用一組相同的 lift() 變換。你可以這麼寫:

observable1
    .lift1()
    .lift2()
    .lift3()
    .lift4()
    .subscribe(subscriber1);
observable2
    .lift1()
    .lift2()
    .lift3()
    .lift4()
    .subscribe(subscriber2);
observable3
    .lift1()
    .lift2()
    .lift3()
    .lift4()
    .subscribe(subscriber3);
observable4
    .lift1()
    .lift2()
    .lift3()
    .lift4()
    .subscribe(subscriber1);複製程式碼

你覺得這樣太不軟體工程了,於是你改成了這樣:

private Observable liftAll(Observable observable) {
    return observable
        .lift1()
        .lift2()
        .lift3()
        .lift4();
}
...
liftAll(observable1).subscribe(subscriber1);
liftAll(observable2).subscribe(subscriber2);
liftAll(observable3).subscribe(subscriber3);
liftAll(observable4).subscribe(subscriber4);複製程式碼

可讀性、可維護性都提高了。可是 Observable 被一個方法包起來,這種方式對於 Observale 的靈活性似乎還是增添了那麼點限制。怎麼辦?這個時候,就應該用 compose() 來解決了:

public class LiftAllTransformer implements Observable.Transformer<Integer, String> {
    @Override
    public Observable<String> call(Observable<Integer> observable) {
        return observable
            .lift1()
            .lift2()
            .lift3()
            .lift4();
    }
}
...
Transformer liftAll = new LiftAllTransformer();
observable1.compose(liftAll).subscribe(subscriber1);
observable2.compose(liftAll).subscribe(subscriber2);
observable3.compose(liftAll).subscribe(subscriber3);
observable4.compose(liftAll).subscribe(subscriber4);複製程式碼

像上面這樣,使用 compose() 方法,Observable 可以利用傳入的 Transformer 物件的 call 方法直接對自身進行處理,也就不必被包在方法的裡面了。

compose() 的原理比較簡單,不附圖嘍。

5. 執行緒控制:Scheduler (二)

除了靈活的變換,RxJava 另一個牛逼的地方,就是執行緒的自由控制。

1) Scheduler 的 API (二)

前面講到了,可以利用 subscribeOn() 結合 observeOn() 來實現執行緒控制,讓事件的產生和消費發生在不同的執行緒。可是在瞭解了 map() flatMap() 等變換方法後,有些好事的(其實就是當初剛接觸 RxJava 時的我)就問了:能不能多切換幾次執行緒?

答案是:能。因為 observeOn() 指定的是 Subscriber 的執行緒,而這個 Subscriber 並不是(嚴格說應該為『不一定是』,但這裡不妨理解為『不是』)subscribe() 引數中的 Subscriber ,而是 observeOn() 執行時的當前 Observable 所對應的 Subscriber ,即它的直接下級 Subscriber 。換句話說,observeOn() 指定的是它之後的操作所在的執行緒。因此如果有多次切換執行緒的需求,只要在每個想要切換執行緒的位置呼叫一次 observeOn() 即可。上程式碼:

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

如上,通過 observeOn() 的多次呼叫,程式實現了執行緒的多次切換。

不過,不同於 observeOn()subscribeOn() 的位置放在哪裡都可以,但它是隻能呼叫一次的。

又有好事的(其實還是當初的我)問了:如果我非要呼叫多次 subscribeOn() 呢?會有什麼效果?

這個問題先放著,我們還是從 RxJava 執行緒控制的原理說起吧。

2) Scheduler 的原理(二)

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

subscribeOn() 原理圖:

subscribeOn() 原理

observeOn() 原理圖:

observeOn() 原理

從圖中可以看出,subscribeOn()observeOn() 都做了執行緒切換的工作(圖中的 “schedule…” 部位)。不同的是, subscribeOn() 的執行緒切換髮生在 OnSubscribe 中,即在它通知上一級 OnSubscribe 時,這時事件還沒有開始傳送,因此 subscribeOn() 的執行緒控制可以從事件發出的開端就造成影響;而 observeOn() 的執行緒切換則發生在它內建的 Subscriber 中,即發生在它即將給下一級 Subscriber 傳送事件時,因此 observeOn() 控制的是它後面的執行緒。

最後,我用一張圖來解釋當多個 subscribeOn()observeOn() 混合使用時,執行緒排程是怎麼發生的(由於圖中物件較多,相對於上面的圖對結構做了一些簡化調整):

執行緒控制綜合呼叫

圖中共有 5 處含有對事件的操作。由圖中可以看出,①和②兩處受第一個 subscribeOn() 影響,執行在紅色執行緒;③和④處受第一個 observeOn() 的影響,執行在綠色執行緒;⑤處受第二個 onserveOn() 影響,執行在紫色執行緒;而第二個 subscribeOn() ,由於在通知過程中執行緒就被第一個 subscribeOn() 截斷,因此對整個流程並沒有任何影響。這裡也就回答了前面的問題:當使用了多個 subscribeOn() 的時候,只有第一個 subscribeOn() 起作用。

3) 延伸:doOnSubscribe()

然而,雖然超過一個的 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 的適用場景和使用方式

1. 與 Retrofit 的結合

Retrofit 是 Square 的一個著名的網路請求庫。沒有用過 Retrofit 的可以選擇跳過這一小節也沒關係,我舉的每種場景都只是個例子,而且例子之間並無前後關聯,只是個拋磚引玉的作用,所以你跳過這裡看別的場景也可以的。

Retrofit 除了提供了傳統的 Callback 形式的 API,還有 RxJava 版本的 Observable 形式 API。下面我用對比的方式來介紹 Retrofit 的 RxJava 版 API 和傳統版本的區別。

以獲取一個 User 物件的介面作為例子。使用Retrofit 的傳統 API,你可以用這樣的方式來定義請求:

@GET("/user")
public void getUser(@Query("userId") String userId, Callback<User> callback);複製程式碼

在程式的構建過程中, Retrofit 會把自動把方法實現並生成程式碼,然後開發者就可以利用下面的方法來獲取特定使用者並處理響應:

getUser(userId, new Callback<User>() {
    @Override
    public void success(User user) {
        userView.setUser(user);
    }

    @Override
    public void failure(RetrofitError error) {
        // Error handling
        ...
    }
};複製程式碼

而使用 RxJava 形式的 API,定義同樣的請求是這樣的:

@GET("/user")
public Observable<User> getUser(@Query("userId") String userId);複製程式碼

使用的時候是這樣的:

getUser(userId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<User>() {
        @Override
        public void onNext(User user) {
            userView.setUser(user);
        }

        @Override
        public void onCompleted() {
        }

        @Override
        public void onError(Throwable error) {
            // Error handling
            ...
        }
    });複製程式碼

看到區別了嗎?

當 RxJava 形式的時候,Retrofit 把請求封裝進 Observable ,在請求結束後呼叫 onNext() 或在請求失敗後呼叫 onError()

對比來看, Callback 形式和 Observable 形式長得不太一樣,但本質都差不多,而且在細節上 Observable 形式似乎還比 Callback 形式要差點。那 Retrofit 為什麼還要提供 RxJava 的支援呢?

因為它好用啊!從這個例子看不出來是因為這只是最簡單的情況。而一旦情景複雜起來, Callback 形式馬上就會開始讓人頭疼。比如:

假設這麼一種情況:你的程式取到的 User 並不應該直接顯示,而是需要先與資料庫中的資料進行比對和修正後再顯示。使用 Callback方式大概可以這麼寫:

getUser(userId, new Callback<User>() {
    @Override
    public void success(User user) {
        processUser(user); // 嘗試修正 User 資料
        userView.setUser(user);
    }

    @Override
    public void failure(RetrofitError error) {
        // Error handling
        ...
    }
};複製程式碼

有問題嗎?

很簡便,但不要這樣做。為什麼?因為這樣做會影響效能。資料庫的操作很重,一次讀寫操作花費 10~20ms 是很常見的,這樣的耗時很容易造成介面的卡頓。所以通常情況下,如果可以的話一定要避免在主執行緒中處理資料庫。所以為了提升效能,這段程式碼可以優化一下:

getUser(userId, new Callback<User>() {
    @Override
    public void success(User user) {
        new Thread() {
            @Override
            public void run() {
                processUser(user); // 嘗試修正 User 資料
                runOnUiThread(new Runnable() { // 切回 UI 執行緒
                    @Override
                    public void run() {
                        userView.setUser(user);
                    }
                });
            }).start();
    }

    @Override
    public void failure(RetrofitError error) {
        // Error handling
        ...
    }
};複製程式碼

效能問題解決,但……這程式碼實在是太亂了,迷之縮排啊!雜亂的程式碼往往不僅僅是美觀問題,因為程式碼越亂往往就越難讀懂,而如果專案中充斥著雜亂的程式碼,無疑會降低程式碼的可讀性,造成團隊開發效率的降低和出錯率的升高。

這時候,如果用 RxJava 的形式,就好辦多了。 RxJava 形式的程式碼是這樣的:

getUser(userId)
    .doOnNext(new Action1<User>() {
        @Override
        public void call(User user) {
            processUser(user);
        })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<User>() {
        @Override
        public void onNext(User user) {
            userView.setUser(user);
        }

        @Override
        public void onCompleted() {
        }

        @Override
        public void onError(Throwable error) {
            // Error handling
            ...
        }
    });複製程式碼

後臺程式碼和前臺程式碼全都寫在一條鏈中,明顯清晰了很多。

再舉一個例子:假設 /user 介面並不能直接訪問,而需要填入一個線上獲取的 token ,程式碼應該怎麼寫?

Callback 方式,可以使用巢狀的 Callback

@GET("/token")
public void getToken(Callback<String> callback);

@GET("/user")
public void getUser(@Query("token") String token, @Query("userId") String userId, Callback<User> callback);

...

getToken(new Callback<String>() {
    @Override
    public void success(String token) {
        getUser(token, userId, new Callback<User>() {
            @Override
            public void success(User user) {
                userView.setUser(user);
            }

            @Override
            public void failure(RetrofitError error) {
                // Error handling
                ...
            }
        };
    }

    @Override
    public void failure(RetrofitError error) {
        // Error handling
        ...
    }
});複製程式碼

倒是沒有什麼效能問題,可是迷之縮排毀一生,你懂我也懂,做過大專案的人應該更懂。

而使用 RxJava 的話,程式碼是這樣的:

@GET("/token")
public Observable<String> getToken();

@GET("/user")
public Observable<User> getUser(@Query("token") String token, @Query("userId") String userId);

...

getToken()
    .flatMap(new Func1<String, Observable<User>>() {
        @Override
        public Observable<User> onNext(String token) {
            return getUser(token, userId);
        })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<User>() {
        @Override
        public void onNext(User user) {
            userView.setUser(user);
        }

        @Override
        public void onCompleted() {
        }

        @Override
        public void onError(Throwable error) {
            // Error handling
            ...
        }
    });複製程式碼

用一個 flatMap() 就搞定了邏輯,依然是一條鏈。看著就很爽,是吧?

2016/03/31 更新,加上我寫的一個 Sample 專案:
rengwuxian RxJava Samples

好,Retrofit 部分就到這裡。

2. RxBinding

RxBinding 是 Jake Wharton 的一個開源庫,它提供了一套在 Android 平臺上的基於 RxJava 的 Binding API。所謂 Binding,就是類似設定 OnClickListener 、設定 TextWatcher 這樣的註冊繫結物件的 API。

舉個設定點選監聽的例子。使用 RxBinding ,可以把事件監聽用這樣的方法來設定:

Button button = ...;
RxView.clickEvents(button) // 以 Observable 形式來反饋點選事件
    .subscribe(new Action1<ViewClickEvent>() {
        @Override
        public void call(ViewClickEvent event) {
            // Click handling
        }
    });複製程式碼

看起來除了形式變了沒什麼區別,實質上也是這樣。甚至如果你看一下它的原始碼,你會發現它連實現都沒什麼驚喜:它的內部是直接用一個包裹著的 setOnClickListener() 來實現的。然而,僅僅這一個形式的改變,卻恰好就是 RxBinding 的目的:擴充套件性。通過 RxBinding把點選監聽轉換成 Observable 之後,就有了對它進行擴充套件的可能。擴充套件的方式有很多,根據需求而定。一個例子是前面提到過的 throttleFirst() ,用於去抖動,也就是消除手抖導致的快速連環點選:

RxView.clickEvents(button)
    .throttleFirst(500, TimeUnit.MILLISECONDS)
    .subscribe(clickAction);複製程式碼

如果想對 RxBinding 有更多瞭解,可以去它的 GitHub 專案 下面看看。

3. 各種非同步操作

前面舉的 RetrofitRxBinding 的例子,是兩個可以提供現成的 Observable 的庫。而如果你有某些非同步操作無法用這些庫來自動生成 Observable,也完全可以自己寫。例如資料庫的讀寫、大圖片的載入、檔案壓縮/解壓等各種需要放在後臺工作的耗時操作,都可以用 RxJava 來實現,有了之前幾章的例子,這裡應該不用再舉例了。

4. RxBus

RxBus 名字看起來像一個庫,但它並不是一個庫,而是一種模式,它的思想是使用 RxJava 來實現了 EventBus ,而讓你不再需要使用 Otto 或者 GreenRobot 的 EventBus。至於什麼是 RxBus,可以看這篇文章。順便說一句,Flipboard 已經用 RxBus 替換掉了 Otto ,目前為止沒有不良反應。

最後

對於 Android 開發者來說, RxJava 是一個很難上手的庫,因為它對於 Android 開發者來說有太多陌生的概念了。可是它真的很牛逼。因此,我寫了這篇《給 Android 開發者的 RxJava 詳解》,希望能給始終搞不明白什麼是 RxJava 的人一些入門的指引,或者能讓正在使用 RxJava 但仍然心存疑惑的人看到一些更深入的解析。無論如何,只要能給各位同為 Android 工程師的你們提供一些幫助,這篇文章的目的就達到了。

原作者部落格地址:http://gank.io/post/560e15be2dca930e00da1083

相關文章