Eventbus 使用方法和原理分析
對於 Eventbus ,相信很多 Android 小夥伴都用到過。
1、建立事件實體類
所謂的事件實體類,就是傳遞的事件,一個元件向另一個元件傳送的資訊可以儲存在一個類中,該類就是一個事件,會被 EventBus 傳送給訂閱者。新建 MessageEvent.java:
public class MessageEvent { private String message; public MessageEvent(String message){ this.message = message; } public String getMessage(){ return message; } }
2、註冊和反註冊
通過以下程式碼:
EventBus.getDefault().register(this);
即可將當前類註冊,成為訂閱者,即對應觀察者模式的“觀察者”,一旦有事件傳送過來,該觀察者就會接收到匹配的事件。通常,在類的初始化時便進行註冊,如果是 Activity 則在的 onCreate()方法內進行註冊。
當訂閱者不再需要接受事件的時候,我們需要解除註冊,釋放記憶體:
EventBus.getDefault().unregister(this);
3、新增訂閱方法
回想觀察者模式,觀察者有著一個 update() 方法,在接收到事件的時候會呼叫該 update() 方法,這個方法就是一個訂閱方法。在EventBus 3.0中,宣告一個訂閱方法需要用到 @Subscribe 註解,因此在訂閱者類中新增一個有著 @Subscribe 註解的方法即可,方法名字可自定義,而且必須是public許可權,其方法引數有且只能有一個,另外型別必須為第一步定義好的事件型別(比如上面的 MessageEvent),如下所示:
@Subscribe public void onEvent(AnyEventType event) { /* Do something */ }
完整的 MainActivity.java 檔案如下所示:
public class MainActivity extends Activity { private TextView textView; private Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //註冊成為訂閱者 EventBus.getDefault().register(this); textView = (TextView) findViewById(R.id.tv_text); button = (Button) findViewById(R.id.secondActivityBtn); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); } }); } //訂閱方法,當接收到事件的時候,會呼叫該方法 @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(MessageEvent messageEvent){ Log.d("cylog","receive it"); textView.setText(messageEvent.getMessage()); Toast.makeText(MainActivity.this, messageEvent.getMessage(), Toast.LENGTH_SHORT).show(); } @Override protected void onDestroy() { super.onDestroy(); //解除註冊 EventBus.getDefault().unregister(this); } }
4、傳送事件
與觀察者模式對應的,當有事件發生,需要通知觀察者的時候,被觀察者會呼叫 notifyObservers() 方法來通知所有已經註冊的觀察者,在 EventBus 中,對觀察者模式底層進行了封裝,我們只需要呼叫以下程式碼就能把事件傳送出去:
EventBus.getDefault().post(EventType eventType);
上述 EventType 就是第一步定義的事件型別。
5、threadMode
POSTING
預設的模式,開銷最小的模式,因為宣告為 POSTING 的訂閱者會在釋出的同一個執行緒呼叫,釋出者在主執行緒那麼訂閱者也就在主執行緒,反之亦,避免了執行緒切換,如果不確定是否有耗時操作,謹慎使用,因為可能是在主執行緒釋出。
MAIN
主執行緒呼叫,視釋出執行緒不同處理不同,如果釋出者在主執行緒那麼直接呼叫(非阻塞式),如果釋出者不在主執行緒那麼阻塞式呼叫,這句話怎麼理解呢,看下面的 Log 比較清晰的理解
主執行緒(阻塞式):
Log.d(TAG, "run : 1"); EventBus.getDefault().post(text);//傳送一個事件 Log.d(TAG, "run : 2"); EventBus.getDefault().post(text);//傳送一個事件 @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent1(String text) { Log.d(TAG, "onMessageEvent1 : "); }
日誌輸出
: run : 1 : onMessageEvent1 : : run : 2 : onMessageEvent1 :
非主執行緒(非阻塞式):
final String text = "長江長江我是黃河"; new Thread(new Runnable() { @Override public void run() { Log.d(TAG, "run : 1"); EventBus.getDefault().post(text);//傳送一個事件 Log.d(TAG, "run : 2"); EventBus.getDefault().post(text);//傳送一個事件 } }).start();
日誌輸出:
run : 1 run : 2 onMessageEvent1 : onMessageEvent1 :
MAIN_ORDERED
和MAIN差不多,主執行緒呼叫,和 MAIN 不同的是他保證了 post 是非阻塞式的(預設走 MAIN 的非主執行緒的邏輯,所以可以做到非阻塞)
BACKGROUND
在子執行緒呼叫,如果釋出在子執行緒那麼直接在釋出執行緒呼叫,如果釋出在主執行緒那麼將開啟一個子執行緒來呼叫,這個子執行緒是阻塞式的,按順序交付所有事件,所以也不適合做耗時任務,因為多個事件共用這一個後臺執行緒
ASYNC
在子執行緒呼叫,總是開啟一個新的執行緒來呼叫,適用於做耗時任務,比如資料庫操作,網路請求等,不適合做計算任務,會導致開啟大量執行緒
6、原理分析:
這裡並不打算分析具體的原始碼邏輯,而是個人在看了原始碼之後的筆記。幫助自己更好的理解 eventbus 的實現原理,梳理清楚每一條邏輯。
想看原始碼分析可以參考這篇文章:
Android EventBus3.1.1從使用到原始碼解析
6.1 單例
/** Convenience singleton for apps using a process-wide EventBus instance. */ public static EventBus getDefault() { if (defaultInstance == null) { synchronized (EventBus.class) { if (defaultInstance == null) { defaultInstance = new EventBus(); } } } return defaultInstance;
這裡在生成單例的時候使用了雙重檢驗,避免多執行緒過程中重複建立。其次這裡使用到了,類縮而非物件鎖。
物件鎖是用來控制例項方法之間的同步,而類鎖是用來控制靜態方法(或者靜態變數互斥體)之間的同步的。
類鎖只是一個概念上的東西,並不是真實存在的,他只是用來幫助我們理解鎖定例項方法和靜態方法的區別的。
java 類可能會有很多物件,但是隻有一個 Class (位元組碼)物件,也就是說類的不同例項之間共享該類的 Class 物件。Class 物件其實也僅僅是 1 個 java 物件,只不過有點特殊而已。
由於每個 java 物件都有1個互斥鎖,而類的靜態方法是需要 Class 物件。所以所謂的類鎖,只不過是 Class 物件的鎖而已。
6.2 以 class 為 key 來儲存方法資訊
例如一個 activity 裡面註冊了一個 eventbus。我們每次進入activity 的時候,都會把 this 傳進去,然後走一遍註冊邏輯,所以你覺得內部是如何儲存註冊物件的呢?是按照 this 來的?
其實內部是通過 class 來儲存的。
public void register(Object subscriber) { Class<?> subscriberClass = subscriber.getClass(); //subscriberMethods返回的是subscriber這個類中所有的訂閱方法 List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass); synchronized (this) { for (SubscriberMethod subscriberMethod : subscriberMethods) { subscribe(subscriber, subscriberMethod);//分類儲存後面有分析 } } }
通過上面程式碼,我們可以看到會去獲取當前物件的類名,然後在通過反射的形式獲取該類的所有方法,從中找到訂閱方法,方便以後釋出訊息。
如果採用物件儲存,每次進入,都是一個不同的物件,然後通過物件再去獲取方法資訊,這樣做太費力,也太耗記憶體了。通過類名的方式,只是第一次比較耗時,後面就方便了。
新增新方法,或者新的事件的時候,會重新編譯,重新獲取一遍新的資料的。
PS : 註冊本身還是掛在物件上的,當物件銷燬的時候,也會進行登出。
6.3 如何儲存訂閱同一事件的不同類
根據事件型別,將註冊同一個事件型別的 class 放在一起。
// Must be called in synchronized block private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) { Class<?> eventType = subscriberMethod.eventType;//訂閱函式引數型別 //這一步很簡單就是在建構函式中記錄下訂閱者和訂閱方法 Subscription newSubscription = new Subscription(subscriber, subscriberMethod); //CopyOnWriteArrayList是java.util包下的,他使用了寫時複製的方法來實現,其效率並不高,但可以保證在多執行緒環境下最終(強調是最終)資料的一致性 CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);//subscriptionsByEventType可以根據引數型別來獲取到訂閱事件 //在操作第一個訂閱事件時肯定是==null的 if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<>(); subscriptionsByEventType.put(eventType, subscriptions); } else { if (subscriptions.contains(newSubscription)) { throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType);//已經註冊的事件不允許再次註冊 } } int size = subscriptions.size(); for (int i = 0; i <= size; i++) { //根據優先順序來新增 if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) { subscriptions.add(i, newSubscription); break; } } //typesBySubscriber可以根據訂閱者來獲取到所有的訂閱方法引數型別 List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber); if (subscribedEvents == null) { subscribedEvents = new ArrayList<>(); typesBySubscriber.put(subscriber, subscribedEvents); } subscribedEvents.add(eventType); if (subscriberMethod.sticky) {//粘性事件的處理邏輯在最後再分析,因為其內容包含了post流程 if (eventInheritance) { // Existing sticky events of all subclasses of eventType have to be considered. // Note: Iterating over all events may be inefficient with lots of sticky events, // thus data structure should be changed to allow a more efficient lookup // (e.g. an additional map storing sub classes of super classes: Class -> List<Class>). Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet(); for (Map.Entry<Class<?>, Object> entry : entries) { Class<?> candidateEventType = entry.getKey(); if (eventType.isAssignableFrom(candidateEventType)) { Object stickyEvent = entry.getValue(); checkPostStickyEventToSubscription(newSubscription, stickyEvent); } } } else { Object stickyEvent = stickyEvents.get(eventType); checkPostStickyEventToSubscription(newSubscription, stickyEvent); } } }
對 subscriptionsByEventType typesBySubscriber 完成資料初始化,subscriptionsByEventType 根據引數型別儲存訂閱者和訂閱方法,typesBySubscriber 根據訂閱者儲存了所有的引數型別,subscriptionsByEventType 主要是 post 時使用,因為其儲存了訂閱者和訂閱事件這兩個引數在反射時要用到,typesBySubscriber 在反註冊時可以根據訂閱者獲取到儲存的事件型別就可以從 subscriptionsByEventType 中獲取到對應的訂閱者和訂閱方法釋放資源,還可以用來判斷是否註冊。
6.4 如何找到訂閱方法
從快取中獲取訂閱方法列表,如果快取中不存在則通過反射獲取到訂閱者所有的函式,遍歷再通過許可權修飾符,引數長度(只允許一個引數),註解(@Subscribe) 來判斷是否是具備成為訂閱函式的前提,具備則構建一個 SubscriberMethod (訂閱方法,其相當於一個數據實體類,包含方法,threadmode,引數型別,優先順序,是否粘性事件這些引數),迴圈結束訂閱函式列表構建完成新增進入快取
6.5 如何在子執行緒釋出訊息後在主執行緒處理
HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) { super(looper); this.eventBus = eventBus; this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage; queue = new PendingPostQueue();//採用獨立佇列,與backgroundPoster一致 }
可以看到,HandlerPoster 自身攜帶一個 looper,主要傳入 mainLooper,就可以處理主執行緒的事物了。
6.6 是如何呼叫訂閱方法的
通過反射的形式呼叫。
void invokeSubscriber(Subscription subscription, Object event) { try { //這裡最後說明一下subscription中包含了訂閱者和訂閱方法 event是Post的引數 這裡通過反射直接呼叫訂閱者的訂閱方法 完成本次通訊 subscription.subscriberMethod.method.invoke(subscription.subscriber, event); } catch (InvocationTargetException e) { handleSubscriberException(subscription, event, e.getCause()); } catch (IllegalAccessException e) { throw new IllegalStateException("Unexpected exception", e); } }
6.7 如何確定優先順序
每次新增的時候,就會根據優先順序來新增,優先順序越高的,新增在最前面。
Subscription newSubscription = new Subscription(subscriber, subscriberMethod); //CopyOnWriteArrayList是java.util包下的,他使用了寫時複製的方法來實現,其效率並不高,但可以保證在多執行緒環境下最終(強調是最終)資料的一致性 CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);//subscriptionsByEventType可以根據引數型別來獲取到訂閱事件 //在操作第一個訂閱事件時肯定是==null的 if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<>(); subscriptionsByEventType.put(eventType, subscriptions); } else { if (subscriptions.contains(newSubscription)) { throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType);//已經註冊的事件不允許再次註冊 } } int size = subscriptions.size(); for (int i = 0; i <= size; i++) { //根據優先順序來新增 if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) { subscriptions.add(i, newSubscription); break; }
}
6.8 既然存在多執行緒,是如何儲存資料的?
public void post(Object event) { //currentPostingThreadState是ThreadLocal,ThreadLocal可以解決多執行緒的併發訪問問題,他會為每一個執行緒提供一個獨立的變數副本,可以隔離多個執行緒對資料的訪問衝突 PostingThreadState postingState = currentPostingThreadState.get();
...... }
ThreadLocal 的是一個本地執行緒副本變數工具類。主要用於將私有執行緒和該執行緒存放的副本物件做一個對映,各個執行緒之間的變數互不干擾,在高併發場景下,可以實現無狀態的呼叫,特別適用於各個執行緒依賴不通的變數值完成操作的場景。
final static class PostingThreadState { // 通過post方法引數傳入的事件集合 final List<Object> eventQueue = new ArrayList<Object>(); boolean isPosting; // 是否正在執行postSingleEvent()方法 boolean isMainThread; Subscription subscription; Object event; boolean canceled; }
Subscription(Object subscriber, SubscriberMethod subscriberMethod) { this.subscriber = subscriber; this.subscriberMethod = subscriberMethod; active = true; }
訂閱方法的資訊:
public SubscriberMethod(String methodName, Class<?> eventType, ThreadMode threadMode, int priority, boolean sticky) { this.methodName = methodName; this.threadMode = threadMode; this.eventType = eventType; this.priority = priority; this.sticky = sticky; }
可以發現,基本上所有的資訊都被包含在 PostingThreadState 中了,這樣在 post 的方法中就不要額外依賴其他資料了。
6.9 傳送訊息邏輯過程是怎樣的
post () 傳送訊息,首先得獲取當前執行緒的一個傳送佇列。從佇列裡面依次取出 event ,根據 event.getClass()來獲取儲存的訂閱者。
synchronized (this) { //這裡根據我們註冊的時候總結 這個容器中裝的是訂閱者和訂閱方法,現在根據傳送事件的型別來獲取到對應的訂閱者和訂閱方法這些引數是反射必須要用到的 subscriptions = subscriptionsByEventType.get(eventClass); }
找到訂閱者以後,依次迴圈,對每個訂閱者進行處理:
//這裡根據是否在主執行緒和threadmode來判斷 private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) { switch (subscription.subscriberMethod.threadMode) { case POSTING: invokeSubscriber(subscription, event);//post在什麼執行緒就直接呼叫 不需要切換執行緒 break; case MAIN: if (isMainThread) { invokeSubscriber(subscription, event); } else { mainThreadPoster.enqueue(subscription, event);//如果Post在主執行緒直接呼叫,反之通過handler來切換到主執行緒再呼叫反射 } break; case MAIN_ORDERED: if (mainThreadPoster != null) {//預設走這裡的邏輯和MAIN一致 事件排隊等待呼叫,非阻塞式 mainThreadPoster.enqueue(subscription, event); } else { // temporary: technically not correct as poster not decoupled from subscriber invokeSubscriber(subscription, event); } break; case BACKGROUND: if (isMainThread) { backgroundPoster.enqueue(subscription, event);//如果在主執行緒則開啟一條執行緒 事件將排隊在同一條執行緒執行 } else {//如果post在子執行緒直接在Post執行緒呼叫 invokeSubscriber(subscription, event); } break; case ASYNC: asyncPoster.enqueue(subscription, event);//總是開啟執行緒來呼叫 break; default: throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode); } }
呼叫 enqueue 後,用於切換執行緒來處理事件,最後還是會通過反射的形式進行呼叫。
6.10 黏性事件如何儲存和傳送
主要使用場景是:當訂閱者尚未建立,先呼叫 EventBus.getDefault().postSticky() 方法傳送一個 sticky 事件,該事件會被 stickyEvents 快取起來,當訂閱該事件的類呼叫 register() 方法時,最終會將儲存的事件全部發給新註冊的訂閱者一份,因此,新的訂閱者同樣可以收到該事。
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) { //獲取subsrciberMethod傳遞的自定義EventType引數的執行時的類 Class eventType = subscriberMethod.eventType; //Subscription用於繫結subscriber和sucriberMethod,一個訂閱者可以有多個subscriberMethod Subscription newSubscription = new Subscription(subscriber, subscriberMethod); //根據EventType的執行時類取到該類所有的subscriptioins,subscriptionsByEventType是HashMap中的key CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType); if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<>(); //若根據EventType找不到subscriptions,則eventType作key,subscriptions作value新增到subscriptionByEventType中。 subscriptionsByEventType.put(eventType, subscriptions); } else { if (subscriptions.contains(newSubscription)) { //已經存在newSubscription,丟擲異常該訂閱者已經註冊,不可重複註冊同一個subscriber throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType); } } int size = subscriptions.size(); for (int i = 0; i <= size; i++) { //迴圈subscriptions,根據標記優先順序的priority從高到低,將新的subscription插入到subscriptions中 if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) { subscriptions.add(i, newSubscription); break; } } //typesBySubscriber是一個HashMap,根據subscriber做key,獲取該subscriber對應的所有的訂閱事件的型別 List> subscribedEvents = typesBySubscriber.get(subscriber); if (subscribedEvents == null) { subscribedEvents = new ArrayList<>(); //該訂閱者之前的訂閱事件型別列表為空,則將當前訂閱型別新增到typesBySubscriber中 typesBySubscriber.put(subscriber, subscribedEvents); } subscribedEvents.add(eventType); //如果該方法被標識為sticky事件 if (subscriberMethod.sticky) { if (eventInheritance) { eventInheritance標識是否考慮EventType的類層次結構 //迴圈所有的sticky黏性事件 Set, Object>> entries = stickyEvents.entrySet(); for (Map.Entry, Object> entry : entries) { Class candidateEventType = entry.getKey(); //如果當前事件是其他事件的同類型的或者是他們的父類 if (eventType.isAssignableFrom(candidateEventType)) { Object stickyEvent = entry.getValue(); heckPostStickyEventToSubscription(newSubscription, stickyEvent); } } } else { Object stickyEvent = stickyEvents.get(eventType); checkPostStickyEventToSubscription(newSubscription, stickyEvent); } } }
從上面我們可以知道,最後都會呼叫一個方法:
private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) { if (stickyEvent != null) { // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state) // --> Strange corner case, which we don't take care of here. postToSubscription(newSubscription, stickyEvent, Looper.getMainLooper() == Looper.myLooper()); } }
最後,也會呼叫到所有事件不管是不是黏性都會走的一個方法:
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) { //根據@subscriber中threadMode進行區分,POSTING為當前執行緒執行, //MAIN為主執行緒,BACKGROUND為子程序,ASYNC為非同步執行。 switch (subscription.subscriberMethod.threadMode) { case POSTING: invokeSubscriber(subscription, event); break; case MAIN: if (isMainThread) { invokeSubscriber(subscription, event); } else { mainThreadPoster.enqueue(subscription, event); } break; case BACKGROUND: if (isMainThread) { backgroundPoster.enqueue(subscription, event); } else { invokeSubscriber(subscription, event); } break; case ASYNC: asyncPoster.enqueue(subscription, event); break; default: throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode); } }
最後呼叫的邏輯還是一樣的。
最後,附上一張 eventbus 的思維導圖,幫助你們更好的去理解 eventbus。