怎麼GET大牛的框架思維?透過EventBus框架告訴你
EventBus的作用
Android中存在各種通訊場景,如
Activity
之間的跳轉,
Activity
與
Fragment
以及其他元件之間的互動,以及在某個耗時操作(如請求網路)之後的callback回撥等,互相之之間往往需要持有對方的引用,每個場景的寫法也有差異,導致耦合性較高且不便維護。
以
Activity
和
Fragment
的通訊為例,官方做法是實現一個介面,然後持有對方的引用,再強行轉成介面型別,導致耦合度偏高。
再以
Activity
的返回為例,一方需要設定
setResult
,而另一方需要在
onActivityResult
做對應處理,如果有多個返回路徑,程式碼就會十分臃腫。而
SimpleEventBus
(本文最終實現的簡化版事件匯流排)的寫法如下:
<pre class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class MainActivity extends AppCompatActivity { TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.tv_demo); mTextView.setText("MainActivity"); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); } }); EventBus.getDefault().register(this); } @Subscribe(threadMode = ThreadMode.MAIN) public void onReturn(Message message) { mTextView.setText(message.mContent); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } }
來源
Activity
:
<pre class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class SecondActivity extends AppCompatActivity { TextView mTextView; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.tv_demo); mTextView.setText("SecondActivity,點選返回"); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Message message = new Message(); message.mContent = "從SecondActivity返回"; EventBus.getDefault().post(message); finish(); } }); } }
效果如下:
似乎只是換了一種寫法,但在場景愈加複雜後,
EventBus
能夠體現出更好的解耦能力
背景知識
主要涉及三方面的知識:
-
觀察者模式(or 釋出-訂閱模式)
-
Android訊息機制
-
Java併發程式設計
實現過程
EventBus`的使用分三個步驟:註冊監聽、傳送事件和取消監聽,相應本文也將分這三步來實現。
註冊監聽
定義一個註解:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Subscribe { ThreadMode threadMode() default ThreadMode.POST; }
greenrobot/EventBus
還支援優先順序和粘性事件,這裡只支援最基本的能力:區分執行緒,因為如更新UI的操作必須放在主執行緒。
ThreadMode`如下:
public enum ThreadMode { MAIN, // 主執行緒 POST, // 傳送訊息的執行緒 ASYNC // 新開一個執行緒傳送}
在物件初始化的時候,使用
register
方法註冊,該方法會解析被註冊物件的所有方法,並解析宣告瞭註解的方法(即觀察者),核心程式碼如下:
public class EventBus { ... public void register(Object subscriber) { if (subscriber == null) { return; } synchronized (this) { subscribe(subscriber); } } ... private void subscribe(Object subscriber) { if (subscriber == null) { return; } // TODO 巨踏馬難看的縮排 Class<?> clazz = subscriber.getClass(); while (clazz != null && !isSystemClass(clazz.getName())) { final Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { Subscribe annotation = method.getAnnotation(Subscribe.class); if (annotation != null) { Class<?>[] paramClassArray = method.getParameterTypes(); if (paramClassArray != null && paramClassArray.length == 1) { Class<?> paramType = convertType(paramClassArray[0]); EventType eventType = new EventType(paramType); SubscriberMethod subscriberMethod = new SubscriberMethod(method, annotation.threadMode(), paramType); realSubscribe(subscriber, subscriberMethod, eventType); } } } clazz = clazz.getSuperclass(); } } ... private void realSubscribe(Object subscriber, SubscriberMethod method, EventType eventType) { CopyOnWriteArrayList<Subscription> subscriptions = mSubscriptionsByEventtype.get(subscriber); if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<>(); } Subscription subscription = new Subscription(subscriber, method); if (subscriptions.contains(subscription)) { return; } subscriptions.add(subscription); mSubscriptionsByEventtype.put(eventType, subscriptions); } ... }
執行過這些邏輯後,該物件所有的觀察者方法都會被存在一個Map中,其Key是
EventType
,即觀察事件的型別,Value是訂閱了該型別事件的所有方法(即觀察者)的一個列表,每個方法和物件一起封裝成了一個
Subscription
類:
public class Subscription { public final Reference<Object> subscriber; public final SubscriberMethod subscriberMethod; public Subscription(Object subscriber, SubscriberMethod subscriberMethod) { this.subscriber = new WeakReference<>(subscriber);// EventBus3 沒用弱引用? this.subscriberMethod = subscriberMethod; } @Override public int hashCode() { return subscriber.hashCode() + subscriberMethod.methodString.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof Subscription) { Subscription other = (Subscription) obj; return subscriber == other.subscribe && subscriberMethod.equals(other.subscriberMethod); } else { return false; } } }
如此,便是註冊監聽方法的核心邏輯了。
訊息傳送
訊息的傳送程式碼很簡單:
public class EventBus { ... private EventDispatcher mEventDispatcher = new EventDispatcher(); private ThreadLocal<Queue<EventType>> mThreadLocalEvents = new ThreadLocal<Queue<EventType>>() { @Override protected Queue<EventType> initialValue() { return new ConcurrentLinkedQueue<>(); } }; ... public void post(Object message) { if (message == null) { return; } mThreadLocalEvents.get().offer(new EventType(message.getClass())); mEventDispatcher.dispatchEvents(message); } ... }
比較複雜一點的是需要根據註解宣告的執行緒模式在對應的執行緒進行釋出:
public class EventBus { ... private class EventDispatcher { private IEventHandler mMainEventHandler = new MainEventHandler(); private IEventHandler mPostEventHandler = new DefaultEventHandler(); private IEventHandler mAsyncEventHandler = new AsyncEventHandler(); void dispatchEvents(Object message) { Queue<EventType> eventQueue = mThreadLocalEvents.get(); while (eventQueue.size() > 0) { handleEvent(eventQueue.poll(), message); } } private void handleEvent(EventType eventType, Object message) { List<Subscription> subscriptions = mSubscriptionsByEventtype.get(eventType); if (subscriptions == null) { return; } for (Subscription subscription : subscriptions) { IEventHandler eventHandler = getEventHandler(subscription.subscriberMethod.threadMode); eventHandler.handleEvent(subscription, message); } } private IEventHandler getEventHandler(ThreadMode mode) { if (mode == ThreadMode.ASYNC) { return mAsyncEventHandler; } if (mode == ThreadMode.POST) { return mPostEventHandler; } return mMainEventHandler; } }// end of the class ... }
三種執行緒模式分別如下,DefaultEventHandler(在釋出執行緒執行觀察者放方法):
public class DefaultEventHandler implements IEventHandler { @Override public void handleEvent(Subscription subscription, Object message) { if (subscription == null || subscription.subscriber.get() == null) { return; } try { subscription.subscriberMethod.method.invoke(subscription.subscriber.get(), message); } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } } }
MainEventHandler(在主執行緒執行):
public class MainEventHandler implements IEventHandler { private Handler mMainHandler = new Handler(Looper.getMainLooper()); DefaultEventHandler hanlder = new DefaultEventHandler(); @Override public void handleEvent(final Subscription subscription, final Object message) { mMainHandler.post(new Runnable() { @Override public void run() { hanlder.handleEvent(subscription, message); } }); } }
AsyncEventHandler(新開一個執行緒執行):
public class AsyncEventHandler implements IEventHandler { private DispatcherThread mDispatcherThread; private IEventHandler mEventHandler = new DefaultEventHandler(); public AsyncEventHandler() { mDispatcherThread = new DispatcherThread(AsyncEventHandler.class.getSimpleName()); mDispatcherThread.start(); } @Override public void handleEvent(final Subscription subscription, final Object message) { mDispatcherThread.post(new Runnable() { @Override public void run() { mEventHandler.handleEvent(subscription, message); } }); } private class DispatcherThread extends HandlerThread { // 關聯了AsyncExecutor訊息佇列的Handle Handler mAsyncHandler; DispatcherThread(String name) { super(name); } public void post(Runnable runnable) { if (mAsyncHandler == null) { throw new NullPointerException("mAsyncHandler == null, please call start() first."); } mAsyncHandler.post(runnable); } @Override public synchronized void start() { super.start(); mAsyncHandler = new Handler(this.getLooper()); } } }
以上便是釋出訊息的程式碼。
登出監聽
最後一個物件被銷燬還要登出監聽,否則容易導致記憶體洩露,目前SimpleEventBus用的是WeakReference,能夠透過GC自動回收,但不知道greenrobot/EventBus為什麼沒這樣實現,待研究。登出監聽其實就是遍歷Map,拿掉該物件的訂閱即可:
public class EventBus { ... public void unregister(Object subscriber) { if (subscriber == null) { return; } Iterator<CopyOnWriteArrayList<Subscription>> iterator = mSubscriptionsByEventtype.values().iterator(); while (iterator.hasNext()) { CopyOnWriteArrayList<Subscription> subscriptions = iterator.next(); if (subscriptions != null) { List<Subscription> foundSubscriptions = new LinkedList<>(); for (Subscription subscription : subscriptions) { Object cacheObject = subscription.subscriber.get(); if (cacheObject == null || cacheObject.equals(subscriber)) { foundSubscriptions.add(subscription); } } subscriptions.removeAll(foundSubscriptions); } // 如果針對某個Event的訂閱者數量為空了,那麼需要從map中清除 if (subscriptions == null || subscriptions.size() == 0) { iterator.remove(); } } } ... }
侷限性
由於時間關係,目前只研究了EventBus的核心部分,還有幾個值得深入研究的點,在此記錄一下,也歡迎路過的大牛指點一二。
效能問題
實際使用時,註解和反射會導致效能問題,但EventBus3已經透過Subscriber Index基本解決了這一問題,實現也非常有意思,是透過註解處理器(Annotation Processor)把耗時的邏輯從執行期提前到了編譯期,透過編譯期生成的索引來給執行期提速,這也是這個名字的由來
可用性問題
如果訂閱者很多會不會影響體驗,畢竟原始的方法是點對點的訊息傳遞,不會有這種問題,如果部分訂閱者尚未初始化怎麼辦。等等。目前EventBus3提供了優先順序和粘性事件的屬性來進一步滿足開發需求。但是否徹底解決問題了還有待驗證。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952849/viewspace-2664095/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 框架思維框架
- 大牛告訴你,只有突破程式設計師思維,才不會淪為碼農!程式設計師
- 大神告訴你如何理解微服務框架微服務框架
- 一張圖告訴你最流行的 7 個 JavaScript 框架特點JavaScript框架
- 軟體行業大牛告訴你何謂成功?行業
- 創新思維框架:第一原則思維 - Neil Kakkar框架
- 告訴你什麼是TestOps測試運維運維
- java (基礎、框架)思維腦圖Java框架
- 幾何約束求解思維框架框架
- [翻譯] Google 大牛告訴你一天時間能學些什麼Go
- java集合框架List介面思維導圖Java框架
- 碼教授告訴你大資料該怎麼用大資料
- 程式猿,讓我來告訴你怎麼追女生!!!
- 年計劃,技術兒告訴你怎麼做?
- 告訴你怎麼選SurfacePro3迎戰MacBookMac
- 讓機器學習告訴你,你的siri在想什麼!機器學習
- 論寫作--思維模式決定文章框架模式框架
- DBA的一天是怎樣的?運維工程師告訴你答案運維工程師
- Xcode重構功能怎麼用我全告訴你XCode
- 都在使用的快取,騰訊大牛告訴你他們是如何使用快取的快取
- 你現在還認為你過去用的框架好用麼框架
- virgil:透過REST訪問Cassandra的開源框架REST框架
- Android EventBus 3.0 框架用法詳解Android框架
- 機器學習中特徵選擇怎麼做?這篇文章告訴你機器學習特徵
- 我來告訴你,ChatGPT 該怎麼對接到自己的專案中!ChatGPT
- 資料告訴你,抖音是怎麼在半年之內逆襲的
- 面試完總是讓你回去等通知?不,告訴你怎麼搶救下!面試
- Android 框架煉成 教你如何寫元件間通訊框架EventBusAndroid框架元件
- 我試圖透過這篇文章告訴你,這行原始碼有多牛逼。原始碼
- 告訴我你的廚房有什麼智慧物件物件
- 透過部署流行 Web 框架掌握 Serverless 技術Web框架Server
- 天下資料告訴你:網站怎麼樣才能做成功網站
- 神經網路是怎麼“看”東西的?啟用圖告訴你神經網路
- 用半勵志的方式告訴你,怎麼學習Python開發Python
- 自己動手實現一個EventBus框架框架
- Vipe框架構思記框架架構
- 利用「系統思維」基本框架,拆解複雜的運營問題框架
- 【轉】kafka-告訴你什麼是kafkaKafka