組件化做爲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()來獲取結果,異步調用獲取時須要本身維護線程。框架
阿里推出的路由引擎,是一個路由框架,並非完整的組件化方案,可做爲組件化架構的通訊引擎。
聚美的路由引擎,在此基礎上也有聚美的組件化實踐方案,基本思想是採用路由 + 接口下沉的方式實現組件化。
美團收銀的組件化方案支持接口調用和消息總線兩種方式,接口調用的方式須要構建CCPData,而後調用ComponentCenter.call,最後在統一的Callback中進行處理。消息總線方式也須要構建CCPData,最後調用ComponentCenter.sendEvent發送。美團收銀的業務組件都打包成AAR上傳至倉庫,組件間存在相互依賴,這樣致使mainapp引用這些組件時須要當心地exclude一些重複依賴。在咱們的組件化方案中,咱們採用了一種巧妙的方法來解決這個問題。
美團App的組件化方案採用ServiceLoader的形式,這是一種典型的接口調用組件通訊方式。用註解定義服務,獲取服務時取得一個接口的List,判斷這個List是否爲空,若是不爲空,則獲取其中一個接口調用。
美團外賣團隊開發的一款Android路由框架,基於組件化的設計思路。主要提供路由、ServiceLoader兩大功能。以前美團技術博客也發表過一篇WMRouter的介紹:《WMRouter:美團外賣Android開源路由框架》。WMRouter提供了實現組件化的兩大基礎設施框架:路由和組件間接口調用。支持和文檔也很充分,能夠考慮做爲咱們團隊實現組件化的基礎設施。
在前期的調研工做中,咱們發現外賣團隊的WMRouter是一個不錯的選擇。首先,WMRouter提供了路由+ServiceLoader兩大組件間通訊功能,其次,WMRouter架構清晰,擴展性比較好,而且文檔和支持也比較完備。因此咱們決定了使用WMRouter做爲組件化基礎設施框架之一。然而,直接使用WMRouter有兩個問題:
在參考了不一樣的組件化方案以後,咱們採用了以下分層結構:
總體架構以下圖所示:
咱們調研其餘組件化方案的時候,發現不少組件方案都是把一個業務模塊拆分紅一個獨立的業務組件,也就是拆分紅一個獨立的Module。而在咱們的方案中,每一個業務組件都拆分紅了一個Export Module和Implement Module,爲何要這樣作呢?
若是採用一個業務組件一個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就能夠了。
在使用單Module方案的組件化方案中,這些業務組件其實不是徹底平等,有些被依賴的組件在層級上要更下沉一些。可是採用Export Module+Implement Module的方案,全部業務組件在層級上徹底平等。
每一個業務組件都劃分紅了Export Module+Implement Module的模式,這個時候每一個Module的功能劃分也更加清晰。Export Module主要定義組件須要對外暴露的部分,主要包含:
Implement Module是組件實現的部分,主要包含:
前文提到的實現組件化基礎設施框架中,咱們用外賣團隊的WMRouter實現頁面路由和組件間接口調用,可是卻沒有消息總線的基礎框架,所以,咱們本身開發了一個組件化消息總線框架modular-event。
以前,咱們開發過一個基於LiveData的消息總線框架:LiveDataBus,也在美團技術博客上發表過一篇文章來介紹這個框架:《Android消息總線的演進之路:用LiveDataBus替代RxBus、EventBus》。關於消息總線的使用,老是伴隨着不少爭論。有些人以爲消息總線很好用,有些人以爲消息總線容易被濫用。
既然已經有了ServiceLoader這種組件間接口調用的框架,爲何還須要消息總線這種方式呢?主要有兩個理由:
基於接口調用的ServiceLoader框架的確實現瞭解耦,可是消息總線可以實現更完全的解耦。接口調用的方式調用方須要依賴這個接口而且知道哪一個組件實現了這個接口。消息總線方式發送者只須要發送一個消息,根本不用關心是否有人訂閱這個消息,這樣發送者根本不須要了解其餘組件的狀況,和其餘組件的耦合也就越少。
基於接口的方式只能進行一對一的調用,基於消息總線的方式可以提供多對多的通訊。
總的來講,消息總線最大的優勢就是解耦,所以很適合組件化這種須要對組件間進行完全解耦的場景。然而,消息總線被不少人詬病的重要緣由,也確實是由於消息總線容易被濫用。消息總線容易被濫用通常體如今幾個場景:
有時候咱們在閱讀代碼的過程當中,找到一個訂閱消息的地方,想要看看是誰發送了這個消息,這個時候每每只能經過查找消息的方式去「溯源」。致使咱們在閱讀代碼,梳理邏輯的過程不太連貫,有種被割裂的感受。
消息總線在發送消息的時候通常沒有強制的約束。不管是EventBus、RxBus或是LiveDataBus,在發送消息的時候既沒有對消息進行檢查,也沒有對發送調用進行約束。這種不規範性在特定的時刻,甚至會帶來災難性的後果。好比訂閱方訂閱了一個名爲login_success的消息,編寫發送消息的是一個比較隨意的程序員,沒有把這個消息定義成全局變量,而是定義了一個臨時變量String發送這個消息。不幸的是,他把消息名稱login_success拼寫成了login_seccess。這樣的話,訂閱方永遠接收不到登陸成功的消息,並且這個錯誤也很難被發現。
之前咱們在使用消息總線時,喜歡把全部的消息都定義到一個公共的Java文件裏面。可是組件化若是也採用這種方案的話,一旦某個組件的消息發生變更,都會去修改這個Java文件。因此咱們但願由組件本身來定義和維護消息定義文件。
若是消息由組件定義和維護,那麼有可能不一樣組件定義了重名的消息,消息總線框架須要可以區分這種消息。
解決消息總線消息難以溯源和消息發送沒有約束的問題。
以前的博文《Android消息總線的演進之路:用LiveDataBus替代RxBus、EventBus》詳細闡述瞭如何基於LiveData構建消息總線。組件化消息總線框架modular-event一樣會基於LiveData構建。使用LiveData構建消息總線有不少優勢:
其實這個問題仍是比較好解決的,實現的方式就是採用兩級HashMap的方式解決。第一級HashMap的構建以ModuleName做爲Key,第二級HashMap做爲Value;第二級HashMap以消息名稱EventName做爲Key,LiveData做爲Value。查找的時候先用組件名稱ModuleName在第一級HashMap中查找,若是找到則用消息名EventName在第二級HashName中查找。整個結構以下圖所示:
咱們但願消息總線框架有如下約束:
整個流程以下圖所示:
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ModuleEvents {
String module() default "";
}
複製代碼
@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";
}
複製代碼
咱們會在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的文章不少,這裏就再也不累述。
有了自動生成的接口,就至關於有了一個殼,然而殼下面的全部邏輯,咱們經過動態代理來實現,簡單介紹一下代理模式和動態代理:
在動態代理的InvocationHandler中實現查找邏輯:
消息的訂閱和發送能夠用鏈式調用的方式編碼:
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"));
複製代碼
本文介紹了美團行業收銀研發組Android團隊的組件化實踐,以及業界獨創強約束組件消息總線modular-event的原理和使用。咱們團隊很早以前就在探索組件化改造,前期有些方案在落地的時候遇到不少困難。咱們也研究了不少開源的組件化方案,以及公司內部其餘團隊(美團App、美團外賣、美團收銀等)的組件化方案,學習和借鑑了不少優秀的設計思想,固然也踩過很多的坑。咱們逐漸意識到:任何一種組件化方案都有其適用場景,咱們的組件化架構選擇,應該更加面向業務,而不只僅是面向技術自己。
咱們的組件化改造工做遠遠沒有結束,將來可能會在如下幾個方向繼續進行深刻的研究:
海亮,美團高級工程師,2017年加入美團,目前主要負責美團輕收銀、美團收銀零售版等App的相關業務及模塊開發工做。
美團餐飲生態誠招Android高級/資深工程師和技術專家,Base北京、成都,歡迎有興趣的同窗投遞簡歷到chenyuxiang@meituan.com。