Android元件化方案及元件訊息匯流排modular-event實戰

美團技術團隊發表於2018-12-21

背景

元件化作為Android客戶端技術的一個重要分支,近年來一直是業界積極探索和實踐的方向。美團內部各個Android開發團隊也在嘗試和實踐不同的元件化方案,並且在元件化通訊框架上也有很多高質量的產出。最近,我們團隊對美團零售收銀和美團輕收銀兩款Android App進行了元件化改造。本文主要介紹我們的元件化方案,希望對從事Android元件化開發的同學能有所啟發。

為什麼要元件化

近年來,為什麼這麼多團隊要進行元件化實踐呢?元件化究竟能給我們的工程、程式碼帶來什麼好處?我們認為元件化能夠帶來兩個最大的好處:

提高元件複用性

可能有些人會覺得,提高複用性很簡單,直接把需要複用的程式碼做成Android Module,打包AAR並上傳程式碼倉庫,那麼這部分功能就能被方便地引入和使用。但是我們覺得僅僅這樣是不夠的,上傳倉庫的AAR庫是否方便被複用,需要元件化的規則來約束,這樣才能提高複用的便捷性。

降低元件間的耦合

我們需要通過元件化的規則把程式碼拆分成不同的模組,模組要做到高內聚、低耦合。模組間也不能直接呼叫,這需要元件化通訊框架的支援。降低了元件間的耦合性可以帶來兩點直接的好處:第一,程式碼更便於維護;第二,降低了模組的Bug率。

元件化之前的狀態

我們的目標是要對團隊的兩款App(美團零售收銀、美團輕收銀)進行元件化重構,那麼這裡先簡單地介紹一下這兩款應用的架構。總的來說,這兩款應用的構架比較相似,主工程Module依賴Business Module,Business Module是各種業務功能的集合,Business Module依賴Service Module,Service Module依賴Platform Module,Service Module和Platform Module都對上層提供服務,有所不同的是Platform Module提供的服務更為基礎,主要包括一些工具Utils和介面Widget,而Service Module提供各種功能服務,如KNB、位置服務、網路介面呼叫等。這樣的話,Business Module就變得非常臃腫和繁雜,各種業務模組相互呼叫,耦合性很強,改業務程式碼時容易“牽一髮而動全身”,即使改一小塊業務程式碼,可能要連帶修改很多相關的地方,不僅在程式碼層面不利於進行維護,而且對一個業務的修改很容易造成其他業務產生Bug。

元件化之前的狀態

元件化方案調研

為了得到最適合我們業態和構架的元件化方案,我們調研了業界開源的一些元件化方案和公司內部其他團隊的元件化方案,在此做個總結。

開源元件化方案調研

我們調研了業界一些主流的開源元件化方案。

號稱業界首個支援漸進式元件化改造的Android元件化開源框架。無論頁面跳轉還是元件間呼叫,都採用CC統一的元件呼叫方式完成。

得到的方案採用路由 + 介面下沉的方式,所有介面下沉到base中,元件中實現介面並在IApplicationLike中新增程式碼註冊到Router中。

元件間呼叫需指定同步實現還是非同步實現,呼叫元件時統一拿到RouterResponse作為返回值,同步呼叫的時候用RouterResponse.getData()來獲取結果,非同步呼叫獲取時需要自己維護執行緒。

阿里推出的路由引擎,是一個路由框架,並不是完整的元件化方案,可作為元件化架構的通訊引擎。

聚美的路由引擎,在此基礎上也有聚美的元件化實踐方案,基本思想是採用路由 + 介面下沉的方式實現元件化。

美團其他團隊元件化方案調研

  • 美團收銀ComponentCenter

美團收銀的元件化方案支援介面呼叫和訊息匯流排兩種方式,介面呼叫的方式需要構建CCPData,然後呼叫ComponentCenter.call,最後在統一的Callback中進行處理。訊息匯流排方式也需要構建CCPData,最後呼叫ComponentCenter.sendEvent傳送。美團收銀的業務元件都打包成AAR上傳至倉庫,元件間存在相互依賴,這樣導致mainapp引用這些元件時需要小心地exclude一些重複依賴。在我們的元件化方案中,我們採用了一種巧妙的方法來解決這個問題。

  • 美團App ServiceLoader

美團App的元件化方案採用ServiceLoader的形式,這是一種典型的介面呼叫元件通訊方式。用註解定義服務,獲取服務時取得一個介面的List,判斷這個List是否為空,如果不為空,則獲取其中一個介面呼叫。

  • WMRouter

美團外賣團隊開發的一款Android路由框架,基於元件化的設計思路。主要提供路由、ServiceLoader兩大功能。之前美團技術部落格也發表過一篇WMRouter的介紹:《WMRouter:美團外賣Android開源路由框架》。WMRouter提供了實現元件化的兩大基礎設施框架:路由和元件間介面呼叫。支援和文件也很充分,可以考慮作為我們團隊實現元件化的基礎設施。

元件化方案

元件化基礎框架

在前期的調研工作中,我們發現外賣團隊的WMRouter是一個不錯的選擇。首先,WMRouter提供了路由+ServiceLoader兩大元件間通訊功能,其次,WMRouter架構清晰,擴充套件性比較好,並且文件和支援也比較完備。所以我們決定了使用WMRouter作為元件化基礎設施框架之一。然而,直接使用WMRouter有兩個問題:

  1. 我們的專案已經在使用一個路由框架,如果使用WMRouter,需要把之前使用的路由框架改成WMRouter路由框架。
  2. WMRouter沒有訊息匯流排框架,我們調研的其他專案也沒有適合我們專案的訊息匯流排框架,因此我們需要開發一個能夠滿足我們需求的訊息匯流排框架,這部分會在後面詳細描述。

元件化分層結構

在參考了不同的元件化方案之後,我們採用瞭如下分層結構:

  1. App殼工程:負責管理各個業務元件和打包APK,沒有具體的業務功能。
  2. 業務元件層:根據不同的業務構成獨立的業務元件,其中每個業務元件包含一個Export Module和Implement Module。
  3. 功能元件層:對上層提供基礎功能服務,如登入服務、列印服務、日誌服務等。
  4. 元件基礎設施:包括WMRouter,提供頁面路由服務和ServiceLoader介面呼叫服務,以及後面會介紹的元件訊息匯流排框架:modular-event。

整體架構如下圖所示:

分層結構

業務元件拆分

我們調研其他元件化方案的時候,發現很多元件方案都是把一個業務模組拆分成一個獨立的業務元件,也就是拆分成一個獨立的Module。而在我們的方案中,每個業務元件都拆分成了一個Export Module和Implement Module,為什麼要這樣做呢?

  1. 避免迴圈依賴

如果採用一個業務元件一個Module的方式,如果Module A需要呼叫Module B提供的介面,那麼Module A就需要依賴Module。同時,如果Module B需要呼叫Module A的介面,那麼Module B就需要依賴Module A。此時就會形成一個迴圈依賴,這是不允許的。

迴圈依賴

也許有些讀者會說,這個好解決:可以把Module A和Module B要依賴的介面放到另一個Module中去,然後讓Module A和Module B都去依賴這個Module就可以了。這確實是一個解決辦法,並且有些專案組在使用這種把介面下沉的方法。

但是我們希望一個元件的介面,是由這個元件自己提供,而不是放在一個更加下沉的介面裡面,所以我們採用了把每個業務元件都拆分成了一個Export Module和Implement Module。這樣的話,如果Module A需要呼叫Module B提供的介面,同時Module B需要呼叫Module A的介面,只需要Module A依賴Module B Export,Module B依賴Module A Export就可以了。

元件結構

  1. 業務元件完全平等

在使用單Module方案的元件化方案中,這些業務元件其實不是完全平等,有些被依賴的元件在層級上要更下沉一些。但是採用Export Module+Implement Module的方案,所有業務元件在層級上完全平等。

  1. 功能劃分更加清晰

每個業務元件都劃分成了Export Module+Implement Module的模式,這個時候每個Module的功能劃分也更加清晰。Export Module主要定義元件需要對外暴露的部分,主要包含:

  • 對外暴露的介面,這些介面用WMRouter的ServiceLoader進行呼叫。
  • 對外暴露的事件,這些事件利用訊息匯流排框架modular-event進行訂閱和分發。
  • 元件的Router Path,元件化之前的工程雖然也使用了Router框架,但是所有Router Path都是定義在了一個下沉Module的公有Class中。這樣導致的問題是,無論哪個模組新增/刪除頁面,或是修改路由,都需要去修改這個公有的Class。設想如果元件化拆分之後,某個元件新增了頁面,還要去一個外部的Java檔案中新增路由,這顯然難以接受,也不符合元件化內聚的目標。因此,我們把每個元件的Router Path放在元件的Export Module中,既可以暴露給其他元件,也可以做到每個元件管理自己的Router Path,不會出現所有元件去修改一個Java檔案的窘境。

Implement Module是元件實現的部分,主要包含:

  • 頁面相關的Activity、Fragment,並且用WMRouter的註解定義路由。
  • Export Module中對外暴露的介面的實現。
  • 其他的業務邏輯。

元件功能劃分

元件化訊息匯流排框架modular-event

前文提到的實現元件化基礎設施框架中,我們用外賣團隊的WMRouter實現頁面路由和元件間介面呼叫,但是卻沒有訊息匯流排的基礎框架,因此,我們自己開發了一個元件化訊息匯流排框架modular-event。

為什麼需要訊息匯流排框架

之前,我們開發過一個基於LiveData的訊息匯流排框架:LiveDataBus,也在美團技術部落格上發表過一篇文章來介紹這個框架:《Android訊息匯流排的演進之路:用LiveDataBus替代RxBus、EventBus》。關於訊息匯流排的使用,總是伴隨著很多爭論。有些人覺得訊息匯流排很好用,有些人覺得訊息匯流排容易被濫用。

既然已經有了ServiceLoader這種元件間介面呼叫的框架,為什麼還需要訊息匯流排這種方式呢?主要有兩個理由:

  1. 更進一步的解耦

基於介面呼叫的ServiceLoader框架的確實現瞭解耦,但是訊息匯流排能夠實現更徹底的解耦。介面呼叫的方式呼叫方需要依賴這個介面並且知道哪個元件實現了這個介面。訊息匯流排方式傳送者只需要傳送一個訊息,根本不用關心是否有人訂閱這個訊息,這樣傳送者根本不需要了解其他元件的情況,和其他元件的耦合也就越少。

  1. 多對多的通訊

基於介面的方式只能進行一對一的呼叫,基於訊息匯流排的方式能夠提供多對多的通訊。

訊息匯流排的優點和缺點

總的來說,訊息匯流排最大的優點就是解耦,因此很適合元件化這種需要對元件間進行徹底解耦的場景。然而,訊息匯流排被很多人詬病的重要原因,也確實是因為訊息匯流排容易被濫用。訊息匯流排容易被濫用一般體現在幾個場景:

  1. 訊息難以溯源

有時候我們在閱讀程式碼的過程中,找到一個訂閱訊息的地方,想要看看是誰傳送了這個訊息,這個時候往往只能通過查詢訊息的方式去“溯源”。導致我們在閱讀程式碼,梳理邏輯的過程不太連貫,有種被割裂的感覺。

  1. 訊息傳送比較隨意,沒有強制的約束

訊息匯流排在傳送訊息的時候一般沒有強制的約束。無論是EventBus、RxBus或是LiveDataBus,在傳送訊息的時候既沒有對訊息進行檢查,也沒有對傳送呼叫進行約束。這種不規範性在特定的時刻,甚至會帶來災難性的後果。比如訂閱方訂閱了一個名為login_success的訊息,編寫傳送訊息的是一個比較隨意的程式設計師,沒有把這個訊息定義成全域性變數,而是定義了一個臨時變數String傳送這個訊息。不幸的是,他把訊息名稱login_success拼寫成了login_seccess。這樣的話,訂閱方永遠接收不到登入成功的訊息,而且這個錯誤也很難被發現。

元件化訊息匯流排的設計目標

  1. 訊息由元件自己定義

以前我們在使用訊息匯流排時,喜歡把所有的訊息都定義到一個公共的Java檔案裡面。但是元件化如果也採用這種方案的話,一旦某個元件的訊息發生變動,都會去修改這個Java檔案。所以我們希望由元件自己來定義和維護訊息定義檔案。

  1. 區分不同元件定義的同名訊息

如果訊息由元件定義和維護,那麼有可能不同元件定義了重名的訊息,訊息匯流排框架需要能夠區分這種訊息。

  1. 解決前文提到的訊息匯流排的缺點

解決訊息匯流排訊息難以溯源和訊息傳送沒有約束的問題。

基於LiveData的訊息匯流排

之前的博文《Android訊息匯流排的演進之路:用LiveDataBus替代RxBus、EventBus》詳細闡述瞭如何基於LiveData構建訊息匯流排。元件化訊息匯流排框架modular-event同樣會基於LiveData構建。使用LiveData構建訊息匯流排有很多優點:

  1. 使用LiveData構建訊息匯流排具有生命週期感知能力,使用者不需要呼叫反註冊,相比EventBus和RxBus使用更為方便,並且沒有記憶體洩漏風險。
  2. 使用普通訊息匯流排,如果回撥的時候Activity處於Stop狀態,這個時候進行彈Dialog一類的操作就會引起崩潰。使用LiveData構建訊息匯流排完全沒有這個風險。

元件訊息匯流排modular-event的實現

解決不同元件定義了重名訊息的問題

其實這個問題還是比較好解決的,實現的方式就是採用兩級HashMap的方式解決。第一級HashMap的構建以ModuleName作為Key,第二級HashMap作為Value;第二級HashMap以訊息名稱EventName作為Key,LiveData作為Value。查詢的時候先用元件名稱ModuleName在第一級HashMap中查詢,如果找到則用訊息名EventName在第二級HashName中查詢。整個結構如下圖所示:

訊息匯流排結構

對訊息匯流排的約束

我們希望訊息匯流排框架有以下約束:

  1. 只能訂閱和傳送在元件中預定義的訊息。換句話說,使用者不能傳送和訂閱臨時訊息。
  2. 訊息的型別需要在定義的時候指定。
  3. 定義訊息的時候需要指定屬於哪個元件。

如何實現這些約束

  1. 在訊息定義檔案上使用註解,定義訊息的型別和訊息所屬Module。
  2. 定義註解處理器,在編譯期間收集訊息的相關資訊。
  3. 在編譯器根據訊息的資訊生成呼叫時需要的interface,用介面約束訊息傳送和訂閱。
  4. 執行時構建基於兩級HashMap的LiveData儲存結構。
  5. 執行時採用interface+動態代理的方式實現真正的訊息訂閱和傳送。

整個流程如下圖所示:

實現流程

訊息匯流排modular-event的結構

  • modular-event-base:定義Anotation及其他基本型別
  • modular-event-core:modular-event核心實現
  • modular-event-compiler:註解處理器
  • modular-event-plugin:Gradle Plugin

Anotation

  • @ModuleEvents:訊息定義
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ModuleEvents {
    String module() default "";
}
  • @EventType:訊息型別
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface EventType {
    Class value();
}

訊息定義

通過@ModuleEvents註解一個定義訊息的Java類,如果@ModuleEvents指定了屬性module,那麼這個module的值就是這個訊息所屬的Module,如果沒有指定屬性module,則會把定義訊息的Java類所在的包的包名作為訊息所屬的Module。

在這個訊息定義java類中定義的訊息都是public static final String型別。可以通過@EventType指定訊息的型別,@EventType支援java原生型別或自定義型別,如果沒有用@EventType指定訊息型別,那麼訊息的型別預設為Object,下面是一個訊息定義的示例:

//可以指定module,若不指定,則使用包名作為module名
@ModuleEvents()
public class DemoEvents {

    //不指定訊息型別,那麼訊息的型別預設為Object
    public static final String EVENT1 = "event1";

    //指定訊息型別為自定義Bean
    @EventType(TestEventBean.class)
    public static final String EVENT2 = "event2";

    //指定訊息型別為java原生型別
    @EventType(String.class)
    public static final String EVENT3 = "event3";
}

interface自動生成

我們會在modular-event-compiler中處理這些註解,一個定義訊息的Java類會生成一個介面,這個介面的命名是EventsDefineOf+訊息定義類名,例如訊息定義類的類名為DemoEvents,自動生成的介面就是EventsDefineOfDemoEvents。訊息定義類中定義的每一個訊息,都會轉化成介面中的一個方法。使用者只能通過這些自動生成的介面使用訊息匯流排。我們用這種巧妙的方式實現了對訊息匯流排的約束。前文提到的那個訊息定義示例DemoEvents.java會生成一個如下的介面類:

package com.sankuai.erp.modularevent.generated.com.meituan.jeremy.module_b_export;

public interface EventsDefineOfDemoEvents extends com.sankuai.erp.modularevent.base.IEventsDefine {
  com.sankuai.erp.modularevent.Observable<java.lang.Object> EVENT1();

  com.sankuai.erp.modularevent.Observable<com.meituan.jeremy.module_b_export.TestEventBean> EVENT2(
      );

  com.sankuai.erp.modularevent.Observable<java.lang.String> EVENT3();
}

關於介面類的自動生成,我們採用了square/javapoet來實現,網上介紹JavaPoet的文章很多,這裡就不再累述。

使用動態代理實現執行時呼叫

有了自動生成的介面,就相當於有了一個殼,然而殼下面的所有邏輯,我們通過動態代理來實現,簡單介紹一下代理模式和動態代理:

  • 代理模式
    給某個物件提供一個代理物件,並由代理物件控制對於原物件的訪問,即客戶不直接操控原物件,而是通過代理物件間接地操控原物件。
  • 動態代理
    代理類是在執行時生成的。也就是說Java編譯完之後並沒有實際的class檔案,而是在執行時動態生成的類位元組碼,並載入到JVM中。

在動態代理的InvocationHandler中實現查詢邏輯:

  1. 根據interface的typename得到ModuleName。
  2. 呼叫的方法的methodname即為訊息名。
  3. 根據ModuleName和訊息名找到相應的LiveData。
  4. 完成後續訂閱訊息或者傳送訊息的流程。

訊息的訂閱和傳送可以用鏈式呼叫的方式編碼:

  • 訂閱訊息
ModularEventBus
        .get()
        .of(EventsDefineOfModuleBEvents.class)
        .EVENT1()
        .observe(this, new Observer<TestEventBean>() {
            @Override
            public void onChanged(@Nullable TestEventBean testEventBean) {
                Toast.makeText(MainActivity.this, "MainActivity收到自定義訊息: " + testEventBean.getMsg(),
                        Toast.LENGTH_SHORT).show();
            }
        });
  • 傳送訊息
ModularEventBus
        .get()
        .of(EventsDefineOfModuleBEvents.class)
        .EVENT1()
        .setValue(new TestEventBean("aa"));

訂閱和傳送的模式

  • 訂閱訊息的模式
  1. observe:生命週期感知,onDestroy的時候自動取消訂閱。
  2. observeSticky:生命週期感知,onDestroy的時候自動取消訂閱,Sticky模式。
  3. observeForever:需要手動取消訂閱。
  4. observeStickyForever:需要手動取消訂閱,Sticky模式。
  • 傳送訊息的模式
  1. setValue:主執行緒呼叫。
  2. postValue:後臺執行緒呼叫。

元件化總結

本文介紹了美團行業收銀研發組Android團隊的元件化實踐,以及業界首創強約束元件訊息匯流排modular-event的原理和使用。我們團隊很早之前就在探索元件化改造,前期有些方案在落地的時候遇到很多困難。我們也研究了很多開源的元件化方案,以及公司內部其他團隊(美團App、美團外賣、美團收銀等)的元件化方案,學習和借鑑了很多優秀的設計思想,當然也踩過不少的坑。我們逐漸意識到:任何一種元件化方案都有其適用場景,我們的元件化架構選擇,應該更加面向業務,而不僅僅是面向技術本身。

後期工作展望

我們的元件化改造工作遠遠沒有結束,未來可能會在以下幾個方向繼續進行深入的研究:

  1. 元件管理:元件化改造之後,每個元件是個獨立的工程,元件也會迭代開發,如何對這些元件進行版本化管理。
  2. 元件重用:現在看起來對這些元件的重用是很方便的,只需要引入元件的庫即可,但是如果一個新的專案到來,需求有些變化,我們應該怎樣最大限度的重用這些元件。
  3. CI整合:如何更好的與CI整合。
  4. 整合到腳手架:整合到腳手架,讓新的專案從一開始就以元件化的模式進行開發。

參考資料

  1. Android訊息匯流排的演進之路:用LiveDataBus替代RxBus、EventBus
  2. WMRouter:美團外賣Android開源路由框架
  3. 美團外賣Android平臺化架構演進實踐

作者簡介

海亮,美團高階工程師,2017年加入美團,目前主要負責美團輕收銀、美團收銀零售版等App的相關業務及模組開發工作。

招聘

美團餐飲生態誠招Android高階/資深工程師和技術專家,Base北京、成都,歡迎有興趣的同學投遞簡歷到chenyuxiang@meituan.com。

相關文章