擁抱 RxJava(三):關於 Observable 的冷熱,常見的封裝方式以及誤區

W_BinaryTree發表於2017-05-04

這一系列文章本來我發表在簡書。最近開始轉移到掘金。以後也會在掘金發表(慢慢拋棄簡書了應該,掘金的技術環境確實比簡書好些)。


前言: 很多朋友誤會我文章的意思。我寫這個系列文章的意思主要是幫助瞭解一下RxJava的常見用法。而不是使用一下自己或別人封裝好的RxBus就覺得自己的專案使用RxJava了。但是這也僅僅是個人口味問題,很多情況下確實RxBus/EventBus會很方便,很刺激,很上癮。所以從這篇文章開始,我把標題中的"放棄RxBus"去除。

無論在簡書,微信平臺,GitHub,掘金等等分享平臺。一個名字上寫著 "MVP(MVVM) + RxJava + Retrofit + Dagger2 + ........"這樣的名字,再熟悉不過了。然而,大多數情況進去看一下RxJava部分。要麼就是簡單的把取到的資料用Observable.just()直接傳給下一層,要麼就是直接使用Retrofit的Adapter來直接獲得Observable,而app中其他部分並沒有reactive。而且還有很多Observable用法錯誤,比如冷熱不分,連續太多的Map/FlatMap等等。

0. RxBus/Retrofit 足夠用了,我為什麼要讓自己的App 更加的Reactive?

為什麼不用RxBus我已經寫了兩篇文章了,可能由於我不常寫文,很多人並沒有理解。在這裡我再解釋一次:EventBus如果是一輛穿梭在所有程式碼之間的公交車。那麼Observable就是穿梭在少許人之間的Uber專車。他比起EventBus有很多優勢,比如型別安全,異常處理,執行緒切換,強大的操作符等等。你當然可以做出一輛超級Uber來當全域性公交車(RxBus)使用,然而這卻損失了RxJava本來的許多優勢,並且又給自己挖了許多坑,得不償失。

0.1 一個常見誤區,過多的operator

剛開始使用RxJava的時候,我們會覺得operator的鏈式呼叫會非常的爽,一個簡單的例子:

Observable.just("1", "2", "3", "4", "5", "6", "7")
          .map(x -> Integer.valueOf(x))
          .map(x -> x * 2)
          .map(x -> x + 4)
          .filter(x -> x >2)
          // and much more operators
          .subscribeOn(Schedulers.io())
          .observeOn(AndroidSchedulers.mainThread());複製程式碼

當你只有很少資料的時候,這樣當然可以,但是你資料量上來的時候,這就會有很多的overhead。 其實幾乎所有的operator都會給你生成一個新的Observable。所以在上面這個例子中,我們在過程中生成了至少7個Observable。然而我們完全可以將中間的.map().map().map().filter合併在一個FlatMap中,減少很多的overhead。

1. Observable.just()的侷限性。

  1. 使用Observable.just() 即使你沒有呼叫subscribe方法。just()括號裡面的程式碼也已經執行了。顯然,Observable.just()不適合封裝網路資料,因為我們通常不想在subscribe之前做網路請求。
    舉個例子:
    class TestClass{
    TestClass(){
     System.out.println("I'm created!");
    }
    }
    Observable.just(new TestClass());複製程式碼
    這時你執行程式碼,你就看到確實你的TestClass 已經被建立了:
    I/System.out: I'm created!複製程式碼
    同理,fromIterable也和just有同樣的缺點。當然,這個可以簡單的用defer()/fromCallable()/create()操作符來是實現只有subscribe只有才載入。
    比如:
    // use fromCallable
    Observable.fromCallable(TestClass::new);
    //or
    Observable.defer(() -> Observable.just(new TestClass()));複製程式碼
  2. Observable.just()不夠靈活。雖然說設計模式上我們追求 "Minimize Mutability" 但是如果我們的程式越來越 reactive的時候。一個 ObservableJust 往往是不滿足需求的。比如之前一定訂閱的subscriber。如果資料更新了,你不可以同過ObservableJust 來通知所有的Observable 新資料更新了,需要你的subscriber主動更新。這顯然有悖於我們追求的reactive programming。 主動pull資料而不是資料告訴你,我更新瞭然後再做出反應。

當然ObservableJust在很多情況下,確實不錯。如果你不需要監聽後續的更新,那麼ObservableJust可以滿足你的需求。

2. Hot Observable 和 cold Observable

這部分是本篇文章的重點!

很多人在封裝資料的時候,並沒有太多考慮冷熱的問題,通常情況下並不會出錯。因為目前很多開源專案(Demo)裡除了RxBus,並沒有太多的RxJava的實時情況。然而,當你的App越來越Reactive的時候,冷熱便是一個必須考慮的問題。
Hot Observable 意思是如果他開始傳輸資料,你不主動喊停(dispose()/cancel()),那麼他就不會停,一直髮射資料,即使他已經沒有Subscriber了。而Cold Observable則是subscribe時才會發射資料。
然而,問題來了。我上篇文章講過,只有subscribeActual方法呼叫了的時候,Observable發射資料,那為什麼Hot Observable沒有Subscriber也會發射資料,他把資料發射給誰了呢?我們在解決這個問題之前,先看一下Cold Observable:

2.1 Cold Observable

我們常見的工廠方法提供的都是ColdObservable,包括just(),fromXX,create(),interval(),defer()。 他們的共同點是當你有多個Subscriber的時候,他們的事件是獨立的,舉個例子:

Observable interval = Observable.interval(1,TimeUnit.SECONDS);複製程式碼

如果我們有兩個subscriber,那麼他們會各自有自己的計時器,並且互不干擾。效果如下圖:

擁抱 RxJava(三):關於 Observable 的冷熱,常見的封裝方式以及誤區
Hot Observable

2.2 Hot Observable

不同於Cold Observable, Hot Observable是共享資料的。對於Hot Observable的所有subscriber,他們會在同一時刻收到相同的資料。我們通常使用publish()操作符來將ColdObservable變為Hot。或者我們在RxBus中常常用到的Subjects 也是Hot Observable。
剛剛我們剛剛提出了一個問題,

既然Hot Observable在沒有subscriber的時候,還會繼續傳送資料,那麼資料究竟發給誰了呢?

其實Hot Observable其實並沒有傳送資料,而是他上層的Observable 傳送資料給這個hot Observable。不信?我們來分別看一下:

2.2.1 ConnectableObservable

我們在上面的誤區中知道了,幾乎所有operator都會生成一個新的Observable。publish當然不例外。但是有區別的是,publish會給你一個ConnectableObservable。具體實現類是ObservablePublish。這個Observable的區別是他提供一個connect()方法,如果你呼叫connect()方法,ConnectableObservable就會開始接收上游Observable的資料。我們來測試一下:

ConnectableObservable interval = Observable.interval(1, TimeUnit.SECONDS).publish();
//connect even when no subscribers
interval.connect();複製程式碼

擁抱 RxJava(三):關於 Observable 的冷熱,常見的封裝方式以及誤區
ConnectableObservable

果然,由於我們subscribe晚了一些。0這個資料沒有收到,當我們兩個 Subscriber 都dispose的時候,ConnectableObservable 也仍在接受資料,導致我們6這個資料沒有接收到。
ConnectableObservable 其實在內部,有一個PublishObserver,他有兩個作用。一個是當我們呼叫 connect()方法時, PublishObserver開始接受上游的資料,我們的例子裡便是 Observable.interval(1, TimeUnit.SECONDS) 。所以才能在我們沒有呼叫 subscribe方法時,他也能開始傳送資料。第二個作用是 PublishObserver儲存所有的下游Subscriber, 也就是我們例子中的Subscriber1 和Subscriber2,在 PublishObserver 每次接到一個上游資料,就會將接收到的結果,依次分發給他儲存的所有 Subscribers ,如果下游 Subscriber 呼叫了 dispose方法,那麼他就會在自己的快取中刪除這個 Subscriber,下次接受到上游資料便不會傳給這個Subscriber
那麼這時候,有同學應該要問了:

我們可不可以停止從上游接受資料?

我們當然可以。 connect()方法會返回一個 Disposable 給我們來控制是否繼續接受上游的資料。

2.2.2 ConnectableObservable的常用操作符

我們當然不希望每次都手動控制 ConnectableObservable的開關。RxJava給我們提供了一些常用的控制操作符

  1. refCount()
    refCount()可以說是最常用的操作符了。他會把 ConnectableObservable變為一個通常的Observable但又保持了HotObservable的特性。也就是說,如果出現第一個Subscriber,他就會自動呼叫 connect()方法,如果他開始接受之後,下游的 Subscribers全部dispose,那麼他也會停止接受上游的資料。具體看圖:

擁抱 RxJava(三):關於 Observable 的冷熱,常見的封裝方式以及誤區
refCount()

每個 Subscriber 每次都會接受同樣的資料,但是當所有 subscriber 都 dispose時候,他也會自動dipose上游的 Observable 。所以我們重新subscribe的時候,又重新從0開始。
這個操作符常用到,RxJava將他和publish合併為一個操作符 :share()

  1. autoConnect()
    autoConnect()看名字就知道,他會自動連結,如果你單純呼叫 autoConnect() ,那麼,他會在你連結第一個 Subscriber 的時候呼叫 connect(),或者你呼叫 autoConnect(int Num),那麼他將會再收到Num個 subscriber的時候連結。
    但是,這個操作符的關鍵在於,由於我們為了鏈式呼叫,autoConnect會返回Observable給你,你不會在返回方法裡獲得一個 Disposable來控制上游的開關。 不過沒問題,autoConnect提供了另一種過載方法 :
    autoConnect(int numberOfSubscribers, Consumer<? super Disposable> connection)
    他會在這個 Consumer傳給你 你需要的那個總開關。而且,autoConnect並不會autoDisconnect, 也就是如果他即使沒有subscriber了。他也會繼續接受資料。
  2. replay()
    replay()方法和 publish()一樣,會返回一個 ConnectableObservable,區別是, replay()會為新的subscriber重放他之前所收到的上游資料,我們再來舉個例子:

    //only replay 3 values
    Observable.interval(1, TimeUnit.SECONDS).replay(3).refCount();複製程式碼

    擁抱 RxJava(三):關於 Observable 的冷熱,常見的封裝方式以及誤區
    replay()

    果然,Subscriber2在subscribe時候,立即收到了之前已經錯過的三個資料,然後繼續接受後面的資料。
    但是,這裡有幾點需要考慮:replay() 會快取上游發過來的資料,所以並不需要擔心重新生成新資料給新的 Subscriber。

  3. ReplayingShare()
    其實ReplayingShare並不能算是ConnectableObservable的一個操作符,他是JakeWhaton的一個開源庫,只有百來行。實現的功能是幾乎和replay(1).refCount()差不多。但是如果中斷 Conncection之後,重新開始subscribe,他仍然會給你一個重放他上一次的結果。 具體看圖:

擁抱 RxJava(三):關於 Observable 的冷熱,常見的封裝方式以及誤區
ReplayingShare()

我們看到和剛才的replay不同,即使兩個Subscriber都 dispose, 重新開始仍然會接收到我們快取過的一個資料。

2.3 Subjects

Subjects 作為一個Reactive世界中的特殊存在,他特殊在於他自己既是一個Observable又是一個Observer(Subscriber)。你既可以像普通Observable一樣讓別的Subscriber來訂閱,也可以用Subjects來訂閱別人。更方便的是他甚至暴露了OnXX(),方法給你。你直接呼叫可以通知所有的Subscriber。 這也是RxBus的基礎,RxBus幾乎離不開Subjects。 蜘蛛俠的老爹告訴我們,力量越大,責任就也大。Subjects也一樣。 Subjects因為暴露了OnXX()方法,使得Subjects的資料來源變得難以控制。而且,Subjects一直是HotObservable,我們來看下Subject的OnNext()方法的實現:

@Override
public void onNext(T t) {
    if (subscribers.get() == TERMINATED) {
        return;
    }
    if (t == null) {
        onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
        return;
    }
    for (PublishDisposable<T> s : subscribers.get()) {
        s.onNext(t);
    }
}複製程式碼

可以看出來Subjects只要呼叫了OnNext()方法就會立即傳送資料。所以,使用時一定要注意Subjects和Subscriber的連結時序問題。具體Subjects的用法我想介紹帖子已經足夠多了。這裡就不贅述了。

3. 在Android中常見的幾種封裝和注意事項

1.封裝View 的Listener

View 的各種Listener 我們常用create方法來封裝,比如OnClickListener:

Observable.create(emitter -> {
    button.setOnClickListener(v -> emitter.onNext("I'm Clicked"));
    emitter.setCancellable(() -> button.setOnClickListener(null));
});複製程式碼

這裡非常關鍵的一點是一定要設定解除繫結,否則你將持續使用這個會造成記憶體洩漏。而且最好配合使用share()。否則只有最後一個Subscriber能收到OnClick。當然,如果不考慮方法數的話,推薦配合使用RxBinding。

而且,用create()方法封裝Listener適合幾乎所有的callback, 並且安全。

2.封裝簡單的資料來源

設想一個場景,我們有一個User類。裡面有我們的使用者名稱,頭像,各種資訊。然而在我們的app中,可能有三四個Fragment/Activity需要根據這個User做出不同的反應。這時我們就可以簡單的使用Subject來封裝User類。

public class UserRepository {
    private User actualUser;

    private Subject<User> subject = ReplaySubject.createWithSize(1);

    /**
     *
     *Get User Data from wherever you want Network/Database etc
     */

    public Observable<User> getUpdate(){
        return subject;
    }

    public void updateUser(User user){
        actualUser = user;
        subject.onNext(actualUser);
    }
}複製程式碼

如果我們某些模組需要這個User,那麼只需要subscribe到這個Repository,如果User有更新,每一個Subscriber都會收到更新後的User並且互相不影響。而且我們使用ReplaySubject,即使有新的Subscriber,也會收到最新的一個User的快取。
但是使用的時候一定要注意,因為用的是Subject.所以在onNext方法中一旦出現了error。那麼所有的Subscriber都將和這個subject斷開了連結。這裡也可以用 RxRelay 代替Subject,簡單來說Relay就是一個沒有onError和onComplete的Subject。

3.簡單的使用concat().first()來處理多來源

Dan Lew在他的部落格Loading data from multiple sources with RxJava 中介紹過他這種處理方法,

// Our sources (left as an exercise for the reader)
Observable<Data> memory = ...;  
Observable<Data> disk = ...;  
Observable<Data> network = ...;

// Retrieve the first source with data
Observable<Data> source = Observable  
  .concat(memory, disk, network)
  .first();複製程式碼

然後在每次做不同請求的時候重新整理快取

Observable<Data> networkWithSave = network.doOnNext(data -> {  
  saveToDisk(data);
  cacheInMemory(data);
});

Observable<Data> diskWithCache = disk.doOnNext(data -> {  
  cacheInMemory(data);
});複製程式碼

具體也可以看這篇簡書,我也不在過多贅述 :RxJava(八)concat符操作處理多資料來源

這裡也說一下這個方法的缺點。 首先,這個只適合一個Item的時候。如果我們有多個Item從這個Observable中流出。 fisrt()操作符只會取第一個。

4.自己繼承Observable 手動寫subscribeActual()方法

這可能是最靈活的寫法?如果你想用RxJava封裝自己的庫,推薦這種方法封裝。因為這樣不僅僅可以有效的進行錯誤處理,並且不會暴露過多邏輯給外面,許多優秀的RxJava相關庫都是這樣封裝,就連RxJava自己也是把一個個的operator封裝成一個個不同的Observable。但是這種方法確實要求很高,要做很多考慮,比如非同步,多執行緒衝突,錯誤處理。對新手不是很推薦。
還是稍微講一個例子: 還是我們的onClickLisnter的封裝:
RxBinding 的 封裝:

final class ViewClickObservable extends Observable<Object> {
  private final View view;

  ViewClickObservable(View view) {
    this.view = view;
  }

  @Override protected void subscribeActual(Observer<? super Object> observer) {
    if (!checkMainThread(observer)) {
      return;
    }
    Listener listener = new Listener(view, observer);
    observer.onSubscribe(listener);
    view.setOnClickListener(listener);
  }

  static final class Listener extends MainThreadDisposable implements OnClickListener {
    private final View view;
    private final Observer<? super Object> observer;

    Listener(View view, Observer<? super Object> observer) {
      this.view = view;
      this.observer = observer;
    }

    @Override public void onClick(View v) {
      if (!isDisposed()) {
        observer.onNext(Notification.INSTANCE);
      }
    }

    @Override protected void onDispose() {
      view.setOnClickListener(null);
    }
  }
}複製程式碼

其實這裡雖然程式碼更多,但是實質上是剛才我們說到的用Observable.create()來封裝沒有很多區別。那我們為什麼還要這麼麻煩自己寫Observable? 因為與create相比,減少了物件個數。 Observable封裝一個OnClickListener 需要 ObservableCreate, ObservableOnSubscribe,ObservableEmitter 這三個類的例項。來確保你封裝出來的Observable 是遵守 Observable Contract的。 而如果你預設自己遵守Observable Contract, 你只需要一個 CustomObservable來實現。減少了兩個物件的生成。 這個觀點也得到了證實,我在StackOverFlow問過相關問題。很幸運得到了RxJava 2.x 作者 David 的回覆:

擁抱 RxJava(三):關於 Observable 的冷熱,常見的封裝方式以及誤區

如果直接繼承與Observable來封裝。大概分如下幾步:

  1. 將你需要監聽的類/介面,通過構造方法傳入這個Observable。比如將我們需要監聽OnClick的View傳入:
    ViewClickObservable(View view) {
     this.view = view;
    }複製程式碼
  2. 將你需要使用的Listener/CallBack 和 Disposable/Observer介面結合成為一個實現類,實現監聽。並且通過構造方法,將下游的Observer傳入,以實現傳輸資料。比如OnClickListener:

    static final class Listener extends MainThreadDisposable implements OnClickListener {
     private final View view;
     private final Observer<? super Object> observer;
    
     Listener(View view, Observer<? super Object> observer) {
       this.view = view;
       this.observer = observer;
     }
    
     @Override public void onClick(View v) {
       if (!isDisposed()) {
         observer.onNext(Notification.INSTANCE);
       }
     }
    
     @Override protected void onDispose() {
       view.setOnClickListener(null);
     }
    }複製程式碼

這裡推薦使用內部類的形式,降低內部可見性。而且這裡需要注意的點非常多。
首先Disposable的 兩個方法都需要實現,isDisposed()一般使用AtomicBoolean來控制監聽是否已經取消訂閱,比如:

    private final AtomicBoolean unsubscribed = new AtomicBoolean();

    @Override
    public final boolean isDisposed() {
        return unsubscribed.get();
    }複製程式碼

dispose()方法一般會放一些取消監聽等方法。比如我們上面看到的view.setOnClickListener(null);。 這裡 onDispose正常是沒有這個方法的。 這個是Jake Wharton為了方便封裝出來的介面,放在dispose方法裡執行的。

public abstract class MainThreadDisposable implements Disposable {
    @Override
    public final void dispose() {
        onDispose();
    }
    protected abstract void onDispose();
}複製程式碼

他當然還在dipose方法裡做了其他安全檢查,消除了一些Boilerplate。

其次,這裡的對於Observer的控制是十分寬鬆的。所以你的行為最好一定遵循Observable Contract。否則出現其他問題,比如下游取消訂閱上游還在傳送資料。 又或者onComplete/onError之後還有onNext出發。 又或者出現異常並不會在onError中得到等等。 這裡推薦看一下RxBinding其他實現類的原始碼。或者是Retrofit RxJava 2 的 Adapter。 都會很有幫助。

最後,如果你是封裝一個生產Observable的方法,那麼使用Disposable。如果你是想封裝一個自定義Operator。那麼需要實現Observer介面。使用這個observer來監聽上游資料。自定義Operator實在複雜。這裡我就不講了,我自己也沒精通這個。

  1. 在SubscribeActual裡,註冊監聽方法,還是我們剛才的例子:
  @Override protected void subscribeActual(Observer<? super Object> observer) {
    if (!checkMainThread(observer)) {
      return;
    }
    Listener listener = new Listener(view, observer);
    observer.onSubscribe(listener);
    view.setOnClickListener(listener);
  }複製程式碼

看了我第二篇文章的朋友這裡應該對subscribeActual方法不陌生。這裡的引數是下游的observer。將他直接傳入我們剛才設計好的Listener/Disposable/Observer。 然後通過observer.onSubscribe註冊我們的disposable。 然後我們開始註冊監聽。這裡順序很重要。一定要先呼叫onSubscribe方法再註冊監聽。否則可能會出現下游disposable空指標異常。

總結

這篇文章在簡書上也發了有一陣子了。轉到掘金時我又重新檢查了下,補充了一些內容。可能有些地方看起來與我之前的文章有些許重複。希望見諒。

相關文章