Android组件化方案及组件消息总线modular-event实战
背景
組件化作為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。
組件化方案調研
為了得到最適合我們業態和構架的組件化方案,我們調研了業界開源的一些組件化方案和公司內部其他團隊的組件化方案,在此做個總結。
開源組件化方案調研
我們調研了業界一些主流的開源組件化方案。
- CC
號稱業界首個支持漸進式組件化改造的Android組件化開源框架。無論頁面跳轉還是組件間調用,都采用CC統一的組件調用方式完成。
- DDComponentForAndroid
得到的方案采用路由 + 接口下沉的方式,所有接口下沉到base中,組件中實現接口并在IApplicationLike中添加代碼注冊到Router中。
- ModularizationArchitecture
組件間調用需指定同步實現還是異步實現,調用組件時統一拿到RouterResponse作為返回值,同步調用的時候用RouterResponse.getData()來獲取結果,異步調用獲取時需要自己維護線程。
- ARouter
阿里推出的路由引擎,是一個路由框架,并不是完整的組件化方案,可作為組件化架構的通信引擎。
- 聚美Router
聚美的路由引擎,在此基礎上也有聚美的組件化實踐方案,基本思想是采用路由 + 接口下沉的方式實現組件化。
美團其他團隊組件化方案調研
美團收銀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有兩個問題:
組件化分層結構
在參考了不同的組件化方案之后,我們采用了如下分層結構:
整體架構如下圖所示:
業務組件拆分
我們調研其他組件化方案的時候,發現很多組件方案都是把一個業務模塊拆分成一個獨立的業務組件,也就是拆分成一個獨立的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就可以了。
2. 業務組件完全平等
在使用單Module方案的組件化方案中,這些業務組件其實不是完全平等,有些被依賴的組件在層級上要更下沉一些。但是采用Export Module+Implement Module的方案,所有業務組件在層級上完全平等。
3. 功能劃分更加清晰
每個業務組件都劃分成了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框架的確實現了解耦,但是消息總線能夠實現更徹底的解耦。接口調用的方式調用方需要依賴這個接口并且知道哪個組件實現了這個接口。消息總線方式發送者只需要發送一個消息,根本不用關心是否有人訂閱這個消息,這樣發送者根本不需要了解其他組件的情況,和其他組件的耦合也就越少。
2. 多對多的通信
基于接口的方式只能進行一對一的調用,基于消息總線的方式能夠提供多對多的通信。
消息總線的優點和缺點
總的來說,消息總線最大的優點就是解耦,因此很適合組件化這種需要對組件間進行徹底解耦的場景。然而,消息總線被很多人詬病的重要原因,也確實是因為消息總線容易被濫用。消息總線容易被濫用一般體現在幾個場景:
1. 消息難以溯源
有時候我們在閱讀代碼的過程中,找到一個訂閱消息的地方,想要看看是誰發送了這個消息,這個時候往往只能通過查找消息的方式去“溯源”。導致我們在閱讀代碼,梳理邏輯的過程不太連貫,有種被割裂的感覺。
2. 消息發送比較隨意,沒有強制的約束
消息總線在發送消息的時候一般沒有強制的約束。無論是EventBus、RxBus或是LiveDataBus,在發送消息的時候既沒有對消息進行檢查,也沒有對發送調用進行約束。這種不規范性在特定的時刻,甚至會帶來災難性的后果。比如訂閱方訂閱了一個名為login_success的消息,編寫發送消息的是一個比較隨意的程序員,沒有把這個消息定義成全局變量,而是定義了一個臨時變量String發送這個消息。不幸的是,他把消息名稱login_success拼寫成了login_seccess。這樣的話,訂閱方永遠接收不到登錄成功的消息,而且這個錯誤也很難被發現。
組件化消息總線的設計目標
1. 消息由組件自己定義
以前我們在使用消息總線時,喜歡把所有的消息都定義到一個公共的Java文件里面。但是組件化如果也采用這種方案的話,一旦某個組件的消息發生變動,都會去修改這個Java文件。所以我們希望由組件自己來定義和維護消息定義文件。
2. 區分不同組件定義的同名消息
如果消息由組件定義和維護,那么有可能不同組件定義了重名的消息,消息總線框架需要能夠區分這種消息。
3. 解決前文提到的消息總線的缺點
解決消息總線消息難以溯源和消息發送沒有約束的問題。
基于LiveData的消息總線
之前的博文《Android消息總線的演進之路:用LiveDataBus替代RxBus、EventBus》詳細闡述了如何基于LiveData構建消息總線。組件化消息總線框架modular-event同樣會基于LiveData構建。使用LiveData構建消息總線有很多優點:
組件消息總線modular-event的實現
解決不同組件定義了重名消息的問題
其實這個問題還是比較好解決的,實現的方式就是采用兩級HashMap的方式解決。第一級HashMap的構建以ModuleName作為Key,第二級HashMap作為Value;第二級HashMap以消息名稱EventName作為Key,LiveData作為Value。查找的時候先用組件名稱ModuleName在第一級HashMap中查找,如果找到則用消息名EventName在第二級HashName中查找。整個結構如下圖所示:
對消息總線的約束
我們希望消息總線框架有以下約束:
如何實現這些約束
整個流程如下圖所示:
消息總線modular-event的結構
- modular-event-base:定義Anotation及其他基本類型
- modular-event-core:modular-event核心實現
- modular-event-compiler:注解處理器
- modular-event-plugin:Gradle Plugin
Anotation
- @ModuleEvents:消息定義
- @EventType:消息類型
消息定義
通過@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 {//不指定消息類型,那么消息的類型默認為Objectpublic 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中實現查找邏輯:
消息的訂閱和發送可以用鏈式調用的方式編碼:
- 訂閱消息
- 發送消息
訂閱和發送的模式
訂閱消息的模式
- observe:生命周期感知,onDestroy的時候自動取消訂閱。
- observeSticky:生命周期感知,onDestroy的時候自動取消訂閱,Sticky模式。
- observeForever:需要手動取消訂閱。
- observeStickyForever:需要手動取消訂閱,Sticky模式。
發送消息的模式
- setValue:主線程調用。
- postValue:后臺線程調用。
總結
本文介紹了美團行業收銀研發組Android團隊的組件化實踐,以及強約束組件消息總線modular-event的原理和使用。我們團隊很早之前就在探索組件化改造,前期有些方案在落地的時候遇到很多困難。我們也研究了很多開源的組件化方案,以及公司內部其他團隊(美團App、美團外賣、美團收銀等)的組件化方案,學習和借鑒了很多優秀的設計思想,當然也踩過不少的坑。我們逐漸意識到:任何一種組件化方案都有其適用場景,我們的組件化架構選擇,應該更加面向業務,而不僅僅是面向技術本身。
后期工作展望
我們的組件化改造工作遠遠沒有結束,未來可能會在以下幾個方向繼續進行深入的研究:
參考資料
作者簡介
- 海亮,美團高級工程師,2017年加入美團,目前主要負責美團輕收銀、美團收銀零售版等App的相關業務及模塊開發工作。
招聘
美團餐飲生態誠招Android高級/資深工程師和技術專家,Base北京、成都,歡迎有興趣的同學投遞簡歷到chenyuxiang@meituan.com。
總結
以上是生活随笔為你收集整理的Android组件化方案及组件消息总线modular-event实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud构建微服务架构:
- 下一篇: 快手高级Java四轮面试题:设计模式+红