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