Android组件化方案及组件消息总线modular-event实战
背景
組件化作為Android客戶端技術的一個重要分支,近年來一直是業(yè)界積極探索和實踐的方向。美團內部各個Android開發(fā)團隊也在嘗試和實踐不同的組件化方案,并且在組件化通信框架上也有很多高質量的產(chǎn)出。最近,我們團隊對美團零售收銀和美團輕收銀兩款Android App進行了組件化改造。本文主要介紹我們的組件化方案,希望對從事Android組件化開發(fā)的同學能有所啟發(fā)。
為什么要組件化
近年來,為什么這么多團隊要進行組件化實踐呢?組件化究竟能給我們的工程、代碼帶來什么好處?我們認為組件化能夠帶來兩個最大的好處。
提高組件復用性
可能有些人會覺得,提高復用性很簡單,直接把需要復用的代碼做成Android Module,打包AAR并上傳代碼倉庫,那么這部分功能就能被方便地引入和使用。但是我們覺得僅僅這樣是不夠的,上傳倉庫的AAR庫是否方便被復用,需要組件化的規(guī)則來約束,這樣才能提高復用的便捷性。
降低組件間的耦合
我們需要通過組件化的規(guī)則把代碼拆分成不同的模塊,模塊要做到高內聚、低耦合。模塊間也不能直接調用,這需要組件化通信框架的支持。降低了組件間的耦合性可以帶來兩點直接的好處:第一,代碼更便于維護;第二,降低了模塊的Bug率。
組件化之前的狀態(tài)
我們的目標是要對團隊的兩款App(美團零售收銀、美團輕收銀)進行組件化重構,那么這里先簡單地介紹一下這兩款應用的架構。
總的來說,這兩款應用的構架比較相似,主工程Module依賴Business Module,Business Module是各種業(yè)務功能的集合,Business Module依賴Service Module,Service Module依賴Platform Module,Service Module和Platform Module都對上層提供服務。
有所不同的是Platform Module提供的服務更為基礎,主要包括一些工具Utils和界面Widget,而Service Module提供各種功能服務,如KNB、位置服務、網(wǎng)絡接口調用等。這樣的話,Business Module就變得非常臃腫和繁雜,各種業(yè)務模塊相互調用,耦合性很強,改業(yè)務代碼時容易“牽一發(fā)而動全身”,即使改一小塊業(yè)務代碼,可能要連帶修改很多相關的地方,不僅在代碼層面不利于進行維護,而且對一個業(yè)務的修改很容易造成其他業(yè)務產(chǎn)生Bug。
組件化方案調研
為了得到最適合我們業(yè)態(tài)和構架的組件化方案,我們調研了業(yè)界開源的一些組件化方案和公司內部其他團隊的組件化方案,在此做個總結。
開源組件化方案調研
我們調研了業(yè)界一些主流的開源組件化方案。
- CC
號稱業(yè)界首個支持漸進式組件化改造的Android組件化開源框架。無論頁面跳轉還是組件間調用,都采用CC統(tǒng)一的組件調用方式完成。
- DDComponentForAndroid
得到的方案采用路由 + 接口下沉的方式,所有接口下沉到base中,組件中實現(xiàn)接口并在IApplicationLike中添加代碼注冊到Router中。
- ModularizationArchitecture
組件間調用需指定同步實現(xiàn)還是異步實現(xiàn),調用組件時統(tǒng)一拿到RouterResponse作為返回值,同步調用的時候用RouterResponse.getData()來獲取結果,異步調用獲取時需要自己維護線程。
- ARouter
阿里推出的路由引擎,是一個路由框架,并不是完整的組件化方案,可作為組件化架構的通信引擎。
- 聚美Router
聚美的路由引擎,在此基礎上也有聚美的組件化實踐方案,基本思想是采用路由 + 接口下沉的方式實現(xiàn)組件化。
美團其他團隊組件化方案調研
美團收銀ComponentCenter
美團收銀的組件化方案支持接口調用和消息總線兩種方式,接口調用的方式需要構建CCPData,然后調用ComponentCenter.call,最后在統(tǒng)一的Callback中進行處理。消息總線方式也需要構建CCPData,最后調用ComponentCenter.sendEvent發(fā)送。美團收銀的業(yè)務組件都打包成AAR上傳至倉庫,組件間存在相互依賴,這樣導致mainapp引用這些組件時需要小心地exclude一些重復依賴。在我們的組件化方案中,我們采用了一種巧妙的方法來解決這個問題。
美團App ServiceLoader
美團App的組件化方案采用ServiceLoader的形式,這是一種典型的接口調用組件通信方式。用注解定義服務,獲取服務時取得一個接口的List,判斷這個List是否為空,如果不為空,則獲取其中一個接口調用。
WMRouter
美團外賣團隊開發(fā)的一款Android路由框架,基于組件化的設計思路。主要提供路由、ServiceLoader兩大功能。之前美團技術博客也發(fā)表過一篇WMRouter的介紹:《WMRouter:美團外賣Android開源路由框架》。WMRouter提供了實現(xiàn)組件化的兩大基礎設施框架:路由和組件間接口調用。支持和文檔也很充分,可以考慮作為我們團隊實現(xiàn)組件化的基礎設施。
組件化方案
組件化基礎框架
在前期的調研工作中,我們發(fā)現(xiàn)外賣團隊的WMRouter是一個不錯的選擇。首先,WMRouter提供了路由+ServiceLoader兩大組件間通信功能,其次,WMRouter架構清晰,擴展性比較好,并且文檔和支持也比較完備。所以我們決定了使用WMRouter作為組件化基礎設施框架之一。然而,直接使用WMRouter有兩個問題:
組件化分層結構
在參考了不同的組件化方案之后,我們采用了如下分層結構:
整體架構如下圖所示:
業(yè)務組件拆分
我們調研其他組件化方案的時候,發(fā)現(xiàn)很多組件方案都是把一個業(yè)務模塊拆分成一個獨立的業(yè)務組件,也就是拆分成一個獨立的Module。而在我們的方案中,每個業(yè)務組件都拆分成了一個Export Module和Implement Module,為什么要這樣做呢?
1. 避免循環(huán)依賴
如果采用一個業(yè)務組件一個Module的方式,如果Module A需要調用Module B提供的接口,那么Module A就需要依賴Module。同時,如果Module B需要調用Module A的接口,那么Module B就需要依賴Module A。此時就會形成一個循環(huán)依賴,這是不允許的。
也許有些讀者會說,這個好解決:可以把Module A和Module B要依賴的接口放到另一個Module中去,然后讓Module A和Module B都去依賴這個Module就可以了。這確實是一個解決辦法,并且有些項目組在使用這種把接口下沉的方法。
但是我們希望一個組件的接口,是由這個組件自己提供,而不是放在一個更加下沉的接口里面,所以我們采用了把每個業(yè)務組件都拆分成了一個Export Module和Implement Module。這樣的話,如果Module A需要調用Module B提供的接口,同時Module B需要調用Module A的接口,只需要Module A依賴Module B Export,Module B依賴Module A Export就可以了。
2. 業(yè)務組件完全平等
在使用單Module方案的組件化方案中,這些業(yè)務組件其實不是完全平等,有些被依賴的組件在層級上要更下沉一些。但是采用Export Module+Implement Module的方案,所有業(yè)務組件在層級上完全平等。
3. 功能劃分更加清晰
每個業(yè)務組件都劃分成了Export Module+Implement Module的模式,這個時候每個Module的功能劃分也更加清晰。Export Module主要定義組件需要對外暴露的部分,主要包含:
- 對外暴露的接口,這些接口用WMRouter的ServiceLoader進行調用。
- 對外暴露的事件,這些事件利用消息總線框架modular-event進行訂閱和分發(fā)。
- 組件的Router Path,組件化之前的工程雖然也使用了Router框架,但是所有Router Path都是定義在了一個下沉Module的公有Class中。這樣導致的問題是,無論哪個模塊添加/刪除頁面,或是修改路由,都需要去修改這個公有的Class。設想如果組件化拆分之后,某個組件新增了頁面,還要去一個外部的Java文件中新增路由,這顯然難以接受,也不符合組件化內聚的目標。因此,我們把每個組件的Router Path放在組件的Export Module中,既可以暴露給其他組件,也可以做到每個組件管理自己的Router Path,不會出現(xiàn)所有組件去修改一個Java文件的窘境。
Implement Module是組件實現(xiàn)的部分,主要包含:
- 頁面相關的Activity、Fragment,并且用WMRouter的注解定義路由。
- Export Module中對外暴露的接口的實現(xiàn)。
- 其他的業(yè)務邏輯。
組件化消息總線框架modular-event
前文提到的實現(xiàn)組件化基礎設施框架中,我們用外賣團隊的WMRouter實現(xiàn)頁面路由和組件間接口調用,但是卻沒有消息總線的基礎框架,因此,我們自己開發(fā)了一個組件化消息總線框架modular-event。
為什么需要消息總線框架
之前,我們開發(fā)過一個基于LiveData的消息總線框架:LiveDataBus,也在美團技術博客上發(fā)表過一篇文章來介紹這個框架:《Android消息總線的演進之路:用LiveDataBus替代RxBus、EventBus》。關于消息總線的使用,總是伴隨著很多爭論。有些人覺得消息總線很好用,有些人覺得消息總線容易被濫用。
既然已經(jīng)有了ServiceLoader這種組件間接口調用的框架,為什么還需要消息總線這種方式呢?主要有兩個理由。
1. 更進一步的解耦
基于接口調用的ServiceLoader框架的確實現(xiàn)了解耦,但是消息總線能夠實現(xiàn)更徹底的解耦。接口調用的方式調用方需要依賴這個接口并且知道哪個組件實現(xiàn)了這個接口。消息總線方式發(fā)送者只需要發(fā)送一個消息,根本不用關心是否有人訂閱這個消息,這樣發(fā)送者根本不需要了解其他組件的情況,和其他組件的耦合也就越少。
2. 多對多的通信
基于接口的方式只能進行一對一的調用,基于消息總線的方式能夠提供多對多的通信。
消息總線的優(yōu)點和缺點
總的來說,消息總線最大的優(yōu)點就是解耦,因此很適合組件化這種需要對組件間進行徹底解耦的場景。然而,消息總線被很多人詬病的重要原因,也確實是因為消息總線容易被濫用。消息總線容易被濫用一般體現(xiàn)在幾個場景:
1. 消息難以溯源
有時候我們在閱讀代碼的過程中,找到一個訂閱消息的地方,想要看看是誰發(fā)送了這個消息,這個時候往往只能通過查找消息的方式去“溯源”。導致我們在閱讀代碼,梳理邏輯的過程不太連貫,有種被割裂的感覺。
2. 消息發(fā)送比較隨意,沒有強制的約束
消息總線在發(fā)送消息的時候一般沒有強制的約束。無論是EventBus、RxBus或是LiveDataBus,在發(fā)送消息的時候既沒有對消息進行檢查,也沒有對發(fā)送調用進行約束。這種不規(guī)范性在特定的時刻,甚至會帶來災難性的后果。比如訂閱方訂閱了一個名為login_success的消息,編寫發(fā)送消息的是一個比較隨意的程序員,沒有把這個消息定義成全局變量,而是定義了一個臨時變量String發(fā)送這個消息。不幸的是,他把消息名稱login_success拼寫成了login_seccess。這樣的話,訂閱方永遠接收不到登錄成功的消息,而且這個錯誤也很難被發(fā)現(xiàn)。
組件化消息總線的設計目標
1. 消息由組件自己定義
以前我們在使用消息總線時,喜歡把所有的消息都定義到一個公共的Java文件里面。但是組件化如果也采用這種方案的話,一旦某個組件的消息發(fā)生變動,都會去修改這個Java文件。所以我們希望由組件自己來定義和維護消息定義文件。
2. 區(qū)分不同組件定義的同名消息
如果消息由組件定義和維護,那么有可能不同組件定義了重名的消息,消息總線框架需要能夠區(qū)分這種消息。
3. 解決前文提到的消息總線的缺點
解決消息總線消息難以溯源和消息發(fā)送沒有約束的問題。
基于LiveData的消息總線
之前的博文《Android消息總線的演進之路:用LiveDataBus替代RxBus、EventBus》詳細闡述了如何基于LiveData構建消息總線。組件化消息總線框架modular-event同樣會基于LiveData構建。使用LiveData構建消息總線有很多優(yōu)點:
組件消息總線modular-event的實現(xiàn)
解決不同組件定義了重名消息的問題
其實這個問題還是比較好解決的,實現(xiàn)的方式就是采用兩級HashMap的方式解決。第一級HashMap的構建以ModuleName作為Key,第二級HashMap作為Value;第二級HashMap以消息名稱EventName作為Key,LiveData作為Value。查找的時候先用組件名稱ModuleName在第一級HashMap中查找,如果找到則用消息名EventName在第二級HashName中查找。整個結構如下圖所示:
對消息總線的約束
我們希望消息總線框架有以下約束:
如何實現(xiàn)這些約束
整個流程如下圖所示:
消息總線modular-event的結構
- modular-event-base:定義Anotation及其他基本類型
- modular-event-core:modular-event核心實現(xiàn)
- 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。消息定義類中定義的每一個消息,都會轉化成接口中的一個方法。使用者只能通過這些自動生成的接口使用消息總線。我們用這種巧妙的方式實現(xiàn)了對消息總線的約束。前文提到的那個消息定義示例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來實現(xiàn),網(wǎng)上介紹JavaPoet的文章很多,這里就不再累述。
使用動態(tài)代理實現(xiàn)運行時調用
有了自動生成的接口,就相當于有了一個殼,然而殼下面的所有邏輯,我們通過動態(tài)代理來實現(xiàn),簡單介紹一下代理模式和動態(tài)代理:
- 代理模式: 給某個對象提供一個代理對象,并由代理對象控制對于原對象的訪問,即客戶不直接操控原對象,而是通過代理對象間接地操控原對象。
- 動態(tài)代理: 代理類是在運行時生成的。也就是說Java編譯完之后并沒有實際的class文件,而是在運行時動態(tài)生成的類字節(jié)碼,并加載到JVM中。
在動態(tài)代理的InvocationHandler中實現(xiàn)查找邏輯:
消息的訂閱和發(fā)送可以用鏈式調用的方式編碼:
- 訂閱消息
- 發(fā)送消息
訂閱和發(fā)送的模式
訂閱消息的模式
- observe:生命周期感知,onDestroy的時候自動取消訂閱。
- observeSticky:生命周期感知,onDestroy的時候自動取消訂閱,Sticky模式。
- observeForever:需要手動取消訂閱。
- observeStickyForever:需要手動取消訂閱,Sticky模式。
發(fā)送消息的模式
- setValue:主線程調用。
- postValue:后臺線程調用。
總結
本文介紹了美團行業(yè)收銀研發(fā)組Android團隊的組件化實踐,以及強約束組件消息總線modular-event的原理和使用。我們團隊很早之前就在探索組件化改造,前期有些方案在落地的時候遇到很多困難。我們也研究了很多開源的組件化方案,以及公司內部其他團隊(美團App、美團外賣、美團收銀等)的組件化方案,學習和借鑒了很多優(yōu)秀的設計思想,當然也踩過不少的坑。我們逐漸意識到:任何一種組件化方案都有其適用場景,我們的組件化架構選擇,應該更加面向業(yè)務,而不僅僅是面向技術本身。
后期工作展望
我們的組件化改造工作遠遠沒有結束,未來可能會在以下幾個方向繼續(xù)進行深入的研究:
參考資料
作者簡介
- 海亮,美團高級工程師,2017年加入美團,目前主要負責美團輕收銀、美團收銀零售版等App的相關業(yè)務及模塊開發(fā)工作。
招聘
美團餐飲生態(tài)誠招Android高級/資深工程師和技術專家,Base北京、成都,歡迎有興趣的同學投遞簡歷到chenyuxiang@meituan.com。
總結
以上是生活随笔為你收集整理的Android组件化方案及组件消息总线modular-event实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud构建微服务架构:
- 下一篇: Android静态代码扫描效率优化与实践