[深入RxBus]:支持Sticky事件

RxBus、EventBus因為解耦太徹底,濫用的話,項目可維護性會越來越低;一些簡單場景更推薦用回調、Subject來代替事件總線。

實際使用場景,如果RxBus,EventBus二選一,我更傾向于使用EventBus, RxJava專注工作流,EventBus專注事件總線,職責更清晰

有段時間沒更了,幾個月前,我寫過一篇實現簡單的RxBus文章: 用RxJava實現事件總線

在實際環境中,你會發現RxBus還是有一些問題的。

  • 你需要RxBus支持Sticky功能。
  • 你會發現在你訂閱了某個事件后,在后續接收到該事件時,處理的過程中發生了異常,你可能會發現后續的事件都接收不到了!

我將分2篇文章分別給出其方案,這篇介紹如何實現Sticky,另外一篇介紹RxBus中的異常處理方案:
深入RxBus:[異常處理]


什么是Sticky事件?

在Android開發中,Sticky事件只指事件消費者在事件發布之后才注冊的也能接收到該事件的特殊類型。Android中就有這樣的實例,也就是Sticky Broadcast,即粘性廣播。正常情況下如果發送者發送了某個廣播,而接收者在這個廣播發送后才注冊自己的Receiver,這時接收者便無法接收到剛才的廣播,為此Android引入了StickyBroadcast,在廣播發送結束后會保存剛剛發送的廣播(Intent),這樣當接收者注冊完Receiver后就可以接收到剛才已經發布的廣播。這就使得我們可以預先處理一些事件,讓有消費者時再把這些事件投遞給消費者。

Subject

我們在實現簡單的RxBus時使用了PublishSubject,其實RxJava提供給開發者4種Subject
PublishSubject,BehaviorSubject ,BehaviorSubject,AsyncSubject。

  1. PublishSubject只會給在訂閱者訂閱的時間點之后的數據發送給觀察者。
  • BehaviorSubject在訂閱者訂閱時,會發送其最近發送的數據(如果此時還沒有收到任何數據,它會發送一個默認值)。

  • ReplaySubject在訂閱者訂閱時,會發送所有的數據給訂閱者,無論它們是何時訂閱的。

  • AsyncSubject只在原Observable事件序列完成后,發送最后一個數據,后續如果還有訂閱者繼續訂閱該Subject, 則可以直接接收到最后一個值。

Subject

從上圖來看,似乎BehaviorSubjectReplaySubject具備Sticky的特性。

BehaviorSubject方案

BehaviorSubject似乎完全符合Sticky的定義,但是你發現了它只能保存最近的那個事件。

有這樣的場景:如果訂閱者A訂閱了Event1,訂閱者B訂閱了Event2,而此時BehaviorSubject事件隊列里是[..., Event2, Event1],當訂閱者訂閱時,因為保存的是最近的事件:即Event1,所以訂閱者B是接收不到Event2的。

解決辦法就是:
每個Event類型都各自創建一個對應的BehaviorSubject,這樣的話資源開銷比較大,并且該Sticky事件總線和普通的RxBus事件總線不能共享,即:普通事件和Sticky事件是獨立的,因為普通事件是基于PublishSubject, 暫時放棄該方案!

ReplaySubject方案

ReplaySubject可以保存發送過的所有數據事件。

因為保存了所有的數據事件,所以不管什么類型的Event,我們只要過濾該類型,并讓其發送最近的那個Event即可滿足Sticky事件了。但是獲取最近的對應事件是個難點,因為最符合需求的操作符takeLast()僅在訂閱事件結束時(即:onCompleted())才會發送最后的那個數據事件,而我們的RxBus正常情況下應該是盡量避免其訂閱事件結束的。(我沒能找到合適的操作符,如果你知道,請告知我)

所以BehaviorSubject也是比較難實現Sticky特性的。

并且,不管是BehaviorSubject還是ReplaySubject,它們還有一個棘手的問題:它們實現的EventBus和普通的RxBus(基于PublishSubject)之間的數據是相互獨立的!

總結:BehaviorSubjectBehaviorSubject都不能天然適合Sticky事件......

使用Map實現Sticky

該方法思路是在原來PublishSubject實現的RxBus基礎上,使用ConcurrentHashMap<事件類型,事件>保存每個事件的最近事件,不僅能實現Sticky特性,最重要的是可以和普通RxBus的事件數據共享,不獨立

因為我們的RxBus是基于PublishSubject的,而RxJava又有4種Subject,而且其中的BehaviorSubjectReplaySubject看起來又符合Sticky特性,所以我們可能會鉆這個牛角尖,理所當然的認為實現Sticky需要通過其他類型的Subject.... (好吧,我鉆進去了...)

這個方案的思路我是根據EventBus的實現想到的,下面是大致流程:

  1. Map的初始化:
   private final Map<Class<?>, Object> mStickyEventMap;

    public RxBus() {
        mBus = new SerializedSubject<>(PublishSubject.create());
        mStickyEventMap = new ConcurrentHashMap<>();
    }

ConcurrentHashMap是一個線程安全的HashMap, 采用stripping lock(分離鎖),效率比HashTable高很多。

  • 在我們postSticky(Event)時,存入Map中:
public void postSticky(Object event) {
     synchronized (mStickyEventMap) {
         mStickyEventMap.put(event.getClass(), event);
     } 
     post(event); 
}
  • 訂閱時toObservableSticky(Class<T> eventType),先從Map中尋找是否包含該類型的事件,如果沒有,則說明沒有Sticky事件要發送,直接訂閱Subject(此時作為被觀察者Observable);如果有,則說明有Sticky事件需要發送,訂閱merge(Subject 和 Sticky事件)
  public <T> Observable<T> toObservableSticky(final Class<T> eventType) {
        synchronized (mStickyEventMap) {
            Observable<T> observable = mBus.ofType(eventType);
            final Object event = mStickyEventMap.get(eventType);

            if (event != null) {
                return observable.mergeWith(Observable.create(new Observable.OnSubscribe<T>() {
                    @Override
                    public void call(Subscriber<? super T> subscriber) {
                        subscriber.onNext(eventType.cast(event));
                    }
                }));
            } else {
                return observable;
            }
        }
    }

merge操作符:可以將多個Observables合并,就好像它們是單個的Observable一樣。

這樣,Sticky的核心功能就完成了,使用上和普通RxBus一樣,通過postSticky()發送事件,toObservableSticky()訂閱事件。

除此之外,我還提供了getStickyEvent(Class<T> eventType),removeStickyEvent(Class<T> eventType),removeAllStickyEvents()方法,供查找、移除對應事件類型的事件、移除全部Sticky事件。

重要的事

在使用Sticky特性時,在不需要某Sticky事件時, 通過removeStickyEvent(Class<T> eventType)移除它,最保險的做法是:在主Activity的onDestroyremoveAllStickyEvents()
因為我們的RxBus是個單例靜態對象,再正常退出app時,該對象依然會存在于JVM,除非進程被殺死,這樣的話導致StickyMap里的數據依然存在,為了避免該問題,需要在app退出時,清理StickyMap。

// 主Activity(一般是棧底Activity)
@Override
protected void onDestroy() {
    super.onDestroy();
    // 移除所有Sticky事件
    RxBus.getDefault().removeAllStickyEvents();
}

完整代碼

下面是支持Sticky的完整RxBus代碼:

/**
 * PublishSubject: 只會把在訂閱發生的時間點之后來自原始Observable的數據發射給觀察者
 * <p>
 * Created by YoKeyword on 2015/6/17.
 */
public class RxBus {
    private static volatile RxBus mDefaultInstance;
    private final Subject<Object, Object> mBus;

    private final Map<Class<?>, Object> mStickyEventMap;

    public RxBus() {
        mBus = new SerializedSubject<>(PublishSubject.create());
        mStickyEventMap = new ConcurrentHashMap<>();
    }

    public static RxBus getDefault() {
        if (mDefaultInstance == null) {
            synchronized (RxBus.class) {
                if (mDefaultInstance == null) {
                    mDefaultInstance = new RxBus();
                }
            }
        }
        return mDefaultInstance;
    }

    /**
     * 發送事件
     */
    public void post(Object event) {
        mBus.onNext(event);
    }

    /**
     * 根據傳遞的 eventType 類型返回特定類型(eventType)的 被觀察者
     */
    public <T> Observable<T> toObservable(Class<T> eventType) {
        return mBus.ofType(eventType);
    }

    /**
     * 判斷是否有訂閱者
     */
    public boolean hasObservers() {
        return mBus.hasObservers();
    }

    public void reset() {
        mDefaultInstance = null;
    }

    /**
     * Stciky 相關
     */

    /**
     * 發送一個新Sticky事件
     */
    public void postSticky(Object event) {
        synchronized (mStickyEventMap) {
            mStickyEventMap.put(event.getClass(), event);
        }
        post(event);
    }

    /**
     * 根據傳遞的 eventType 類型返回特定類型(eventType)的 被觀察者
     */
    public <T> Observable<T> toObservableSticky(final Class<T> eventType) {
        synchronized (mStickyEventMap) {
            Observable<T> observable = mBus.ofType(eventType);
            final Object event = mStickyEventMap.get(eventType);

            if (event != null) {
                 return observable.mergeWith(Observable.create(new Observable.OnSubscribe<T>() {
                    @Override
                    public void call(Subscriber<? super T> subscriber) {
                        subscriber.onNext(eventType.cast(event));
                    }
                }));
            } else {
                return observable;
            }
        }
    }

    /**
     * 根據eventType獲取Sticky事件
     */
    public <T> T getStickyEvent(Class<T> eventType) {
        synchronized (mStickyEventMap) {
            return eventType.cast(mStickyEventMap.get(eventType));
        }
    }

    /**
     * 移除指定eventType的Sticky事件
     */
    public <T> T removeStickyEvent(Class<T> eventType) {
        synchronized (mStickyEventMap) {
            return eventType.cast(mStickyEventMap.remove(eventType));
        }
    }

    /**
     * 移除所有的Sticky事件
     */
    public void removeAllStickyEvents() {
        synchronized (mStickyEventMap) {
            mStickyEventMap.clear();
        }
    }
}

雖然使用了線程安全的ConCurrentHashMap,但是依然大量使用了synchronized,可以發現鎖住的是mStickyEventMap對象,這是為了保證對于讀、寫、查、刪同步,即讀時不能寫,寫時不能讀...

最后

附上一個Demo:

  • 提供了使用Sticky特性的示例
  • 異常處理的示例:讓其在發生異常后,仍能正確接收到后續的Event。

參考源碼,傳送門

Demo

參考:

ReactiveX: http://reactivex.io/
EventBus: https://github.com/greenrobot/EventBus

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容