Android組件化方案及組件消息總線modular-event實戰

背景

組件化做爲Android客戶端技術的一個重要分支,近年來一直是業界積極探索和實踐的方向。美團內部各個Android開發團隊也在嘗試和實踐不一樣的組件化方案,而且在組件化通訊框架上也有不少高質量的產出。最近,咱們團隊對美團零售收銀和美團輕收銀兩款Android App進行了組件化改造。本文主要介紹咱們的組件化方案,但願對從事Android組件化開發的同窗能有所啓發。html

爲何要組件化

近年來,爲何這麼多團隊要進行組件化實踐呢?組件化究竟能給咱們的工程、代碼帶來什麼好處?咱們認爲組件化可以帶來兩個最大的好處:java

提升組件複用性

可能有些人會以爲,提升複用性很簡單,直接把須要複用的代碼作成Android Module,打包AAR並上傳代碼倉庫,那麼這部分功能就能被方便地引入和使用。可是咱們以爲僅僅這樣是不夠的,上傳倉庫的AAR庫是否方便被複用,須要組件化的規則來約束,這樣才能提升複用的便捷性。android

下降組件間的耦合

咱們須要經過組件化的規則把代碼拆分紅不一樣的模塊,模塊要作到高內聚、低耦合。模塊間也不能直接調用,這須要組件化通訊框架的支持。下降了組件間的耦合性能夠帶來兩點直接的好處:第一,代碼更便於維護;第二,下降了模塊的Bug率。git

組件化以前的狀態

咱們的目標是要對團隊的兩款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。程序員

組件化以前的狀態

組件化方案調研

爲了獲得最適合咱們業態和構架的組件化方案,咱們調研了業界開源的一些組件化方案和公司內部其餘團隊的組件化方案,在此作個總結。github

開源組件化方案調研

咱們調研了業界一些主流的開源組件化方案。網絡

號稱業界首個支持漸進式組件化改造的Android組件化開源框架。不管頁面跳轉仍是組件間調用,都採用CC統一的組件調用方式完成。架構

獲得的方案採用路由 + 接口下沉的方式,全部接口下沉到base中,組件中實現接口並在IApplicationLike中添加代碼註冊到Router中。app

組件間調用需指定同步實現仍是異步實現,調用組件時統一拿到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。

相關文章
相關標籤/搜索