自己動手寫事件匯流排(EventBus)

来源:https://www.cnblogs.com/qcloud1001/archive/2018/12/29/10195195.html
-Advertisement-
Play Games

本文由雲+社區發表 事件匯流排核心邏輯的實現。 <! more EventBus的作用 Android中存在各種通信場景,如 之間的跳轉, 與`Fragment Activity Fragment Activity setResult onActivityResult SimpleEventBus`( ...


本文由雲+社區發表

事件匯流排核心邏輯的實現。

EventBus的作用

Android中存在各種通信場景,如Activity之間的跳轉,ActivityFragment以及其他組件之間的交互,以及在某個耗時操作(如請求網路)之後的callback回調等,互相之之間往往需要持有對方的引用,每個場景的寫法也有差異,導致耦合性較高且不便維護。以ActivityFragment的通信為例,官方做法是實現一個介面,然後持有對方的引用,再強行轉成介面類型,導致耦合度偏高。再以Activity的返回為例,一方需要設置setResult,而另一方需要在onActivityResult做對應處理,如果有多個返迴路徑,代碼就會十分臃腫。而SimpleEventBus(本文最終實現的簡化版事件匯流排)的寫法如下:


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


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能夠體現出更好的解耦能力。

背景知識

主要涉及三方面的知識:

  1. 觀察者模式(or 發佈-訂閱模式)

  2. Android消息機制

  3. Java併發編程

本文可以認為是greenrobot/EventBus這個開源庫的源碼閱讀指南,筆者在看設計模式相關書籍的時候瞭解到這個庫,覺得有必要實現一下核心功能以加深理解。

實現過程

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();

            }

        }

    }

    ...

}

以上便是事件匯流排最核心部分的代碼實現,完整代碼見vimerzhao/SimpleEventBus,後面發現問題更新或者進行升級也只會改動倉庫的代碼。

局限性

由於時間關係,目前只研究了EventBus的核心部分,還有幾個值得深入研究的點,在此記錄一下,也歡迎路過的大牛指點一二。

性能問題

實際使用時,註解和反射會導致性能問題,但EventBus3已經通過Subscriber Index基本解決了這一問題,實現也非常有意思,是通過註解處理器(Annotation Processor)把耗時的邏輯從運行期提前到了編譯期,通過編譯期生成的索引來給運行期提速,這也是這個名字的由來。

可用性問題

如果訂閱者很多會不會影響體驗,畢竟原始的方法是點對點的消息傳遞,不會有這種問題,如果部分訂閱者尚未初始化怎麼辦。等等。目前EventBus3提供了優先順序和粘性事件的屬性來進一步滿足開發需求。但是否徹底解決問題了還有待驗證。

跨進程問題

EventBus是進程內的消息管理機制,並且從開源社區的反饋來看這個項目是非常成功的,但稍微有點體量的APP都做了進程拆分,所以是否有必要支持多進程,能否在保證性能的情況下提供同等的代碼解耦能力,也值得繼續挖掘。目前有lsxiao/ApolloXiaofei-it/HermesEventBus等可供參考。

參考

此文已由作者授權騰訊雲+社區發佈



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • FTP 伺服器架設: 1. 關閉防火牆 2. 關閉SELinux 3. 安裝所需依賴及編譯工具 4. 下載pure ftpd 5. 解壓 6. 進行配置 7. 編譯和安裝 8. 修改配置文件 9. 控制文件 10. 啟動服務 11. 添加管理用戶 12. 創建虛擬的用戶資料庫 13. 通過ftp連接 ...
  • MySQL資料庫是一款比較常用的資料庫,大家在練習安裝時,可能會遇到各種各樣的問題,請大家參考在CentOS系統下MySQL資料庫的安裝方式。如有任何問題,歡迎留言,本人隨時解答。 MySQL安裝步驟如下: 第一步:上傳 MySQL二進位安裝包 MySQL版本:5.6.3 下載鏈接:https:// ...
  • 1、使用索引的已有順序 2、filesort演算法 filesort演算法的執行流程 filesort相關的參數 sort_buffer_size 演算法排序緩衝區的大小,線程級緩存 max_length_for_sort_data 決定選擇那種不同的策略進行排序(兩種排序演算法) 1、two-pass演算法 ...
  • mysql優化–explain分析sql語句執行效率 Explain命令在解決資料庫性能上是第一推薦使用命令,大部分的性能問題可以通過此命令來簡單的解決,Explain可以用來查看SQL語句的執行效 果,可以幫助選擇更好的索引和優化查詢語句,寫出更好的優化語句。 Explain語法:explain ...
  • 正文 之前的博文當中提到備份工具mydumper的使用,而軟體包中還包含了與之對應的恢復工具myloader,本文就總結下myloader的用法。關於mydumper的安裝與使用可以參考之前的博文:[MySQL Backup mydumper][1]。 查看myloader的版本信息: 主要選項 d ...
  • 首先在這裡發發牢騷,指責下那些刻板的書寫方式,不考慮讀者理不理解,感覺就是給專業人員用來複慣用的一樣,沒有前戲,直接就高潮,實在受不了!沒基礎或基礎差的完全不知道發生了什麼,一臉懵逼的看著,一星差評!!! execute immediate 以下引用介紹比較好的例子說明 看了上面的代碼,是否覺得理解 ...
  • 觸發器的基礎知識和例子 :create trigger tr_name on table/view {for | after | instead of } [update][,][insert][,][delete] [with encryption] as {batch | if update ( ...
  • 當前資料庫中創建新的資料庫角色註意事項 角色是資料庫級別的安全對象。 在創建角色後,可以使用 grant、deny 和revoke來配置角色的資料庫級許可權。 若要向資料庫角色添加成員,請使用alter role(Transact-SQL)。 在 sys.database_role_members 和 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...