開源一個自用的Android事件分發中心庫,實現類似系統廣播功能。
由於上一篇文章 《開源一個自用的Android IM庫,基於Netty+TCP+Protobuf實現。》 得到了不錯的反響,激發了寫作的興趣,趁著時間空閒,決定繼續寫一些文章,以下這篇,是一個自定義的Android事件分發中心庫,實現類似 系統廣播 、 EventBus 、 RxBus 的事件釋出-訂閱功能,後續有時間,會繼續完善之前的 NettyChat 庫,包括加入 WebSocket協議實現 、 UDP協議實現 以及大家需要的UI頁面的封裝(包含會話列表頁、訊息列表頁等),敬請期待。
本文的CEventCenter基於 物件池 及 介面回撥 實現,主要解決在Activity/Fragment/Service之間的訊息事件傳遞問題,由於作者水平有限,文中有不對之處歡迎批評與指正。
老規矩,先來看看效果:

接下來,讓我們進入正題。
為什麼不用BroadcastReceiver?
首先,BroadcastReceiver是重量級的、消耗資源較多的方式。其次,我們都知道,onReceive()方法是在主執行緒執行的,執行時間不能超過 10 秒,否則會導致ANR。那麼,大家可能會有疑惑,直接在onReceive()中開啟一個子執行緒處理耗時任務不就可以了嗎?這種方式,不能說不行,只能說並不可靠。Receiver只在onReceive方法執行時是啟用狀態,只要onReceive一返回,Receiver就不再是啟用狀態了。由於activity可能會被使用者退出,Broadcast Receiver的生命週期本身就很短,可能出現的情況是:
在子執行緒還沒有結束的情況下,Activity已經被使用者退出了,或者BroadcastReceiver已經結束了。在Activity已經退出、BroadcastReceiver已經結束的情況下,此時它們所在的程序就變成了空程序(沒有任何活動元件的程序),系統需要記憶體時可能會優先終止該程序。如果宿主程序被終止,那麼該程序內的所有子執行緒也會被中止,這樣就可能導致子執行緒無法執行完成。
以上摘自: 為什麼不能在BroadcastReceiver中開啟子執行緒
為什麼不用RxBus、EventBus?
其實我也有在用,記得在17年初的時候,我們當時在做一個直播專案,其中的訊息事件傳遞,就是用的RxBus,當時是這樣的:觀眾給主播送禮,是通過im給服務端傳送訊息,服務端收到送禮訊息後,處理送禮的邏輯,然後給客戶端返回送禮的狀態訊息,客戶端收到訊息後,通過RxBus把訊息傳遞到Activity(其實這裡不管是通過im還是http介面請求,都存在相同的問題)。在壓測的時候,是每個50ms送一個禮物,很大的概率會出現一個bug,就是下圖這個:

出現這個bug,是因為我們當時用的RxBus版本,內部是使用的RxJava1.0,而RxJava1.0是有一個設計缺陷的,也就是不支援背壓,簡單地說,丟擲MissingBackpressureException往往就是因為,被觀察者傳送事件的速度太快,而觀察者處理太慢,而且你還沒有做相應措施,所以報異常。
當時心態炸了啊,因為專案比較龐大,撇棄RxBus的話,那工作量將非常巨大,而且當時專案著急上線,無奈之下,只能把可能出現這個bug的所有地方,替換成自己實現的CEventCenter,後續再逐步逐步替換...當然了,目前的RxJava已經修復了背壓的問題,而CEventCenter在那之後也一直在使用,目前來說暫時沒發現有什麼問題,所以也就保持在用了,大家如果採用此庫,在使用過程中如果發現問題,煩請聯絡我,我個人也是不提倡重複造輪子的,如果目前有比較好用的庫,那就沒必要重新開發一個,當然,如果時間允許,自己寫一個其實也不錯,至少在這過程中,一定會有所收穫。當然了,EventBus的執行緒模型設計和粘性事件的支援是非常好的。
什麼是物件池?為什麼要使用物件池?
上面有提到,CEventCenter是基於物件池及介面回撥實現的,那麼,什麼是物件池?其實大家應該都使用過OkHttp,瞭解過原始碼的應該都知道,OkHttp原始碼裡面,就有一種叫做連線池的東西,而物件池,跟連線池類似。
在java中,物件的生命週期包括物件建立、物件使用,物件消失三個時間段,其中物件的使用是物件真正需要存活的時間,不好修改,該用的時候還得使用啊。物件的建立和消失就得好好控制下了。物件的建立是比較費時間的,也許感覺不到,好比一個賦值操作int i=1,也是需要耗時的,在比如構造一個物件,一個數組就更加消耗時間。再說物件的消除,在 java 裡面使用GC來進行物件回收,其實也是需要對物件監控每一個執行狀態,包括引用,賦值等。在Full GC的時候,會暫停其他操作,獨佔CPU。所以,我們需要控制物件的建立數量,也不要輕易的讓物件消失,讓他的複用更加充分。
以上摘自:java 物件池技術
廢話不多說,直接開始吧。
首先,定義一個物件池中使用的物件介面:
PooledObject.java
package com.freddy.event; /** * <p>@ProjectName:CEventCenter</p> * <p>@ClassName:PooledObject.java</p> * <p>@PackageName:com.freddy.event</p> * <b> * <p>@Description:物件池中的物件要求實現PooledObject介面</p> * </b> * <p>@author:FreddyChen</p> * <p>@date:2019/04/25 16:59</p> * <p>@email:[email protected]</p> */ public interface PooledObject { /** * 恢復到預設狀態 */ void reset(); } 複製程式碼
然後,定義一個事件模型,也就是需要傳遞的訊息事件物件:
CEvent.java
package com.freddy.event; /** * <p>@ProjectName:CEventCenter</p> * <p>@ClassName:CEvent.java</p> * <p>@PackageName:com.freddy.event</p> * <b> * <p>@Description:事件模型</p> * </b> * <p>@author:FreddyChen</p> * <p>@date:2019/04/25 17:24</p> * <p>@email:[email protected]</p> */ public class CEvent implements PooledObject { private String topic;// 主題 private int msgCode;// 訊息型別 private int resultCode; // 預留引數 private Object obj;// 回撥返回資料 public CEvent() { } public CEvent(String topic, int msgCode, int resultCode, Object obj) { this.topic = topic; this.msgCode = msgCode; this.resultCode = resultCode; this.obj = obj; } public String getTopic() { return topic; } public void setTopic(String topic) { this.topic = topic; } public int getMsgCode() { return msgCode; } public void setMsgCode(int msgCode) { this.msgCode = msgCode; } public int getResultCode() { return resultCode; } public void setResultCode(int resultCode) { this.resultCode = resultCode; } public Object getObj() { return obj; } public void setObj(Object obj) { this.obj = obj; } /** * 恢復到預設狀態 */ @Override public void reset() { this.topic = null; this.msgCode = 0; this.resultCode = 0; this.obj = null; } } 複製程式碼
接下來,自定義一個物件池:
ObjectPool.java
package com.freddy.event; /** * <p>@ProjectName:CEventCenter</p> * <p>@ClassName:ObjectPool.java</p> * <p>@PackageName:com.freddy.event</p> * <b> * <p>@Description:自定義的物件池</p> * </b> * <p>@author:FreddyChen</p> * <p>@date:2019/04/25 17:30</p> * <p>@email:[email protected]</p> */ public abstract class ObjectPool<T extends PooledObject> { private T[] mContainer;// 物件容器 private final Object LOCK = new Object();// 物件鎖 private int length;// 每次返回物件都放到資料末端,length表示前面可用物件數 public ObjectPool(int capacity) { mContainer = createObjPool(capacity); } /** * 建立物件池 * * @param capacity 最大限度容量 * @return 物件池 */ protected abstract T[] createObjPool(int capacity); /** * 建立一個新的物件 * * @return */ protected abstract T createNewObj(); /** * 從物件池中撈出一個物件,如果池已滿,會重新建立一個物件 * * @return */ public final T get() { // 先從池中找到空閒的物件,如果沒有,則重新建立一個物件 T obj = findFreeObject(); if (null == obj) { obj = createNewObj(); } else { // 清除物件狀態 obj.reset(); } return obj; } /** * 從池中找到空閒的物件 * * @return */ private T findFreeObject() { T obj = null; synchronized (LOCK) { if (length > 0) { --length; obj = mContainer[length]; // 賦值完成後,釋放資源 mContainer[length] = null; } } return obj; } /** * 把物件放回池裡面 * * @param obj 需要放回物件池的物件 */ public final void returnObj(T obj) { synchronized (LOCK) { int size = mContainer.length; if (length < size) { mContainer[length] = obj; length++; } } } } 複製程式碼
然後,自定義一個事件物件池,繼承自定義物件池:
CEventPool.java
package com.freddy.event; /** * <p>@ProjectName:CEventCenter</p> * <p>@ClassName:CEventObjPool.java</p> * <p>@PackageName:com.freddy.event</p> * <b> * <p>@Description:事件物件池</p> * </b> * <p>@author:FreddyChen</p> * <p>@date:2019/04/25 17:45</p> * <p>@email:[email protected]</p> */ public class CEventObjPool extends ObjectPool<CEvent> { public CEventObjPool(int capacity) { super(capacity); } @Override protected CEvent[] createObjPool(int capacity) { return new CEvent[capacity]; } @Override protected CEvent createNewObj() { return new CEvent(); } } 複製程式碼
還有事件監聽器:
I_CEventListener.java
package com.freddy.event; /** * <p>@ProjectName:CEventCenter</p> * <p>@ClassName:I_CEventListener.java</p> * <p>@PackageName:com.freddy.event</p> * <b> * <p>@Description:事件監聽器</p> * </b> * <p>@author:FreddyChen</p> * <p>@date:2019/04/25 17:52</p> * <p>@email:[email protected]</p> */ public interface I_CEventListener { /** * 事件回撥函式 * <b>注意:</b><br /> * 如果 obj 使用了物件池,<br /> * 那麼事件完成後,obj即自動回收到物件池,請不要再其它執行緒繼續使用,否則可能會導致資料不正常 * @param topic * @param msgCode * @param resultCode * @param obj */ void onCEvent(String topic, int msgCode, int resultCode, Object obj); } 複製程式碼
最後,就是我們的主角了,事件分發中心:
CEventCenter.java
package com.freddy.event; import android.text.TextUtils; import android.util.Log; import java.util.HashMap; import java.util.LinkedList; import java.util.List; /** * <p>@ProjectName:CEventCenter</p> * <p>@ClassName:CEventCenter.java</p> * <p>@PackageName:com.freddy.event</p> * <b> * <p>@Description:類描述</p> * </b> * <p>@author:FreddyChen</p> * <p>@date:2019/04/25 17:48</p> * <p>@email:[email protected]</p> */ public class CEventCenter { private static final String TAG = CEventCenter.class.getSimpleName(); /** * 監聽器列表,支援一對多儲存 */ private static final HashMap<String, Object> LISTENER_MAP = new HashMap<>(); /** * 監聽器列表鎖 */ private static final Object LISTENER_LOCK = new Object(); /** * 事件物件池 */ private static final CEventObjPool POOL = new CEventObjPool(5); /** * 註冊/登出監聽器 * * @param toBindtrue註冊false登出 * @param listener 監聽器 * @param topic單個主題 */ public static void onBindEvent(boolean toBind, I_CEventListener listener, String topic) { onBindEvent(toBind, listener, new String[]{topic}); } /** * 註冊/登出監聽器 * * @param toBindtrue註冊false登出 * @param listener 監聽器 * @param topics多個主題 */ public static void onBindEvent(boolean toBind, I_CEventListener listener, String[] topics) { if (toBind) { registerEventListener(listener, topics); } else { unregisterEventListener(listener, topics); } } /** * 註冊監聽器 * * @param listener 監聽器 * @param topic單個主題 */ public static void registerEventListener(I_CEventListener listener, String topic) { registerEventListener(listener, new String[]{topic}); } /** * 註冊監聽器 * * @param listener 監聽器 * @param topics多個主題 */ public static void registerEventListener(I_CEventListener listener, String[] topics) { if (null == listener || null == topics) { return; } synchronized (LISTENER_LOCK) { for (String topic : topics) { if (TextUtils.isEmpty(topic)) { continue; } Object obj = LISTENER_MAP.get(topic); if (null == obj) { // 還沒有監聽器,直接放到Map集合 LISTENER_MAP.put(topic, listener); } else if (obj instanceof I_CEventListener) { // 有一個監聽器 I_CEventListener oldListener = (I_CEventListener) obj; if (listener == oldListener) { // 去重 continue; } LinkedList<I_CEventListener> list = new LinkedList<>(); list.add(oldListener); list.add(listener); LISTENER_MAP.put(topic, list); } else if (obj instanceof List) { // 有多個監聽器 LinkedList<I_CEventListener> listeners = (LinkedList<I_CEventListener>) obj; if (listeners.indexOf(listener) >= 0) { // 去重 continue; } listeners.add(listener); } } } } /** * 登出監聽器 * * @param listener 監聽器 * @param topic單個主題 */ public static void unregisterEventListener(I_CEventListener listener, String topic) { unregisterEventListener(listener, new String[]{topic}); } /** * 登出監聽器 * * @param listener 監聽器 * @param topics多個主題 */ public static void unregisterEventListener(I_CEventListener listener, String[] topics) { if (null == listener || null == topics) { return; } synchronized (LISTENER_LOCK) { for (String topic : topics) { if (TextUtils.isEmpty(topic)) { continue; } Object obj = LISTENER_MAP.get(topic); if (null == obj) { continue; } else if (obj instanceof I_CEventListener) { // 有一個監聽器 if (obj == listener) { LISTENER_MAP.remove(topic); } } else if (obj instanceof List) { // 有多個監聽器 LinkedList<I_CEventListener> listeners = (LinkedList<I_CEventListener>) obj; listeners.remove(listener); } } } } /** * 同步分發事件 * * @param topic主題 * @param msgCode訊息型別 * @param resultCode 預留引數 * @param obj回撥返回資料 */ public static void dispatchEvent(String topic, int msgCode, int resultCode, Object obj) { if (!TextUtils.isEmpty(topic)) { CEvent event = POOL.get(); event.setTopic(topic); event.setMsgCode(msgCode); event.setResultCode(resultCode); event.setObj(obj); dispatchEvent(event); } } /** * 同步分發事件 * * @param event */ public static void dispatchEvent(CEvent event) { // 沒有監聽器,直接跳出程式碼,無需執行以下程式碼 if (LISTENER_MAP.size() == 0) { return; } if (null != event && !TextUtils.isEmpty(event.getTopic())) { String topic = event.getTopic(); // 通知事件監聽器處理事件 I_CEventListener listener = null; LinkedList<I_CEventListener> listeners = null; synchronized (LISTENER_LOCK) { Log.d(TAG, "dispatchEvent | topic = " + topic + "\tmsgCode = " + event.getMsgCode() + "\tresultCode = " + event.getResultCode() + "\tobj = " + event.getObj()); Object obj = LISTENER_MAP.get(topic); if (obj == null) { return; } if (obj instanceof I_CEventListener) { listener = (I_CEventListener) obj; } else if (obj instanceof LinkedList) { listeners = (LinkedList<I_CEventListener>) ((LinkedList) obj).clone(); } } // 分發事件 if (null != listener) { listener.onCEvent(topic, event.getMsgCode(), event.getResultCode(), event.getObj()); } else if (null != listeners && listeners.size() > 0) { for (I_CEventListener l : listeners) { l.onCEvent(topic, event.getMsgCode(), event.getResultCode(), event.getObj()); } } // 把物件放回池裡面 POOL.returnObj(event); } } } 複製程式碼
程式碼比較簡單,就是 註冊監聽器 -> 分發事件 -> 事件回撥響應 -> 登出監聽器 四個步驟,支援一對一、一對多釋出主題事件,事件分發完畢後,把物件放回物件池裡面,便於物件複用。
使用方法,拿Activity舉例吧:
- 在A Activity的onCreate()方法中,呼叫 CEventCenter.registerEventListener(I_CEventListener listener, String topic/String[] topics) 方法註冊監聽器,A Activity需要 實現I_CEventListener介面 , 重寫onCEvent(String topic, int msgCode, int resultCode, Object obj)方法 ,同時不要忘記在onDestroy()方法中 呼叫CEventCenter.unregisterEventListener(I_CEventListener listener, String topic/String[] topics) 方法登出監聽器,如下圖:
我們來看看執行效果:

可以看到,在B Activity釋出事件後,A Activity中的TextView文字改變了。另外,多個事件監聽器註冊/登出用法都一樣,只是把String替換成String[]即可,就不介紹啦。
寫在最後
這篇文章比較簡單,由於之前的文章貼了大量的圖片,導致可能載入過慢,而且在手機上看程式碼截圖不是那麼方便,所以這篇文章大部分換成了直接貼原始碼的方式,方便大家閱讀。這個庫實現的功能比較簡單,所以原始碼也能全部貼上來了,如果此庫對你有用,希望在github上給我一個star哈。。。另外,歡迎fork,期望大家與我一起完善。後續會陸續的形式開源一些平時工作中積累的、Android實用的庫,希望大家會喜歡。。。
另外,建立了一個Android即時通訊技術交流QQ群: 1015178804 ,有需要的同學可以加進來,不懂的問題,我會盡量解答,一起學習,一起成長。
The end.