android:組件化方案

(轉(zhuǎn)載)

Android組件化方案已經(jīng)開源,參見Android組件化方案開源。方案的解讀文章是一個(gè)小的系列,這是系列的第二篇文章:

1、Android徹底組件化方案實(shí)踐

2、Android徹底組件化demo發(fā)布

3、Android徹底組件化-代碼和資源隔離

4、Android徹底組件化—UI跳轉(zhuǎn)升級改造

5、Android徹底組件化—如何使用Arouter

今年開始,我開始負(fù)責(zé)對自家的android代碼進(jìn)行組件化拆分,在動手之前我查閱了很多組件化或者模塊化的文章,雖然有一些收獲,但是很少有文章能夠給出一個(gè)整體且有效的方案,大部分文章都只停留在組件單獨(dú)調(diào)試的層面上,涉及組件之間的交互就很少了,更不用說組件生命周期、集成調(diào)試和代碼邊界這些最棘手的問題了。有感于此,我覺得很有必要設(shè)計(jì)一套完整的組件化方案,經(jīng)過幾周的思考,反復(fù)的推倒重建,終于形成了一個(gè)完整的思路,整理在我的第一篇文章中Android徹底組件化方案實(shí)踐。這兩個(gè)月以來,Android團(tuán)隊(duì)按照這個(gè)方案開始了組件化的拆分,經(jīng)過兩個(gè)開發(fā)周期的努力,目前已經(jīng)拆分五個(gè)大的業(yè)務(wù)組件以及數(shù)個(gè)底層lib庫,并對之前的方案進(jìn)行了一些完善。從使用效果上來看,這套方案完全可以達(dá)到了我們之前對組件化的預(yù)期,并且架構(gòu)簡單,學(xué)習(xí)成本低,對于一個(gè)急需快速組件化拆分的項(xiàng)目是很適合的。現(xiàn)在將這套方案開源出來,歡迎大家共同完善。代碼地址:https://github.com/mqzhangw/JIMU

一、JIMU使用指南

首先我們看一下demo的代碼結(jié)構(gòu),然后根據(jù)這個(gè)結(jié)構(gòu)圖再次從單獨(dú)調(diào)試(發(fā)布)、組件交互、UI跳轉(zhuǎn)、集成調(diào)試、代碼邊界和生命周期等六個(gè)方面深入分析,之所以說“再次”,是因?yàn)樯弦黄恼挛覀円呀?jīng)講了這六個(gè)方面的原理,這篇文章更側(cè)重其具體實(shí)現(xiàn)。

JIMU結(jié)構(gòu)圖.png

代碼中的各個(gè)module基本和圖中對應(yīng),從上到下依次是:

app是主項(xiàng)目,負(fù)責(zé)集成眾多組件,控制組件的生命周期

reader和share是我們拆分的兩個(gè)組件

componentservice中定義了所有的組件提供的服務(wù)

basicres定義了全局通用的theme和color等公共資源

basiclib中是公共的基礎(chǔ)庫,一些第三方的庫(okhttp等)也統(tǒng)一交給basiclib來引入

圖中沒有體現(xiàn)的module有兩個(gè),一個(gè)是componentlib,這個(gè)是我們組件化的基礎(chǔ)庫,像Router/UIRouter等都定義在這里;另一個(gè)是build-gradle,這個(gè)是我們組件化編譯的gradle插件,也是整個(gè)組件化方案的核心。

我們在demo中要實(shí)現(xiàn)的場景是:主項(xiàng)目app集成reader和share兩個(gè)組件,其中reader提供一個(gè)讀書的fragment給app調(diào)用(組件交互),share提供一個(gè)activity來給reader來調(diào)用(UI跳轉(zhuǎn))。主項(xiàng)目app可以動態(tài)的添加和卸載share組件(生命周期)。而集成調(diào)試和代碼邊界是通過build-gradle插件來實(shí)現(xiàn)的。

1 單獨(dú)調(diào)試和發(fā)布

單獨(dú)調(diào)試的配置與上篇文章基本一致,通過在組件工程下的gradle.properties文件中設(shè)置一個(gè)isRunAlone的變量來區(qū)分不同的場景,唯一的不同點(diǎn)是在組件的build.gradle中不需要寫下面的樣板代碼:

if(isRunAlone.toBoolean()){? ? apply plugin:'com.android.application'}else{? apply plugin:'com.android.library'}

而只需要引入一個(gè)插件com.dd.comgradle(源碼就在build-gradle),在這個(gè)插件中會自動判斷apply com.android.library還是com.android.application。實(shí)際上這個(gè)插件還能做更“智能”的事情,這個(gè)在集成調(diào)試章節(jié)中會詳細(xì)闡述。

單獨(dú)調(diào)試所必須的AndroidManifest.xml、application、入口activity等類定義在src/main/runalone下面,這個(gè)比較簡單就不贅述了。

如果組件開發(fā)并測試完成,需要發(fā)布一個(gè)release版本的aar文件到中央倉庫,只需要把isRunAlone修改為false,然后運(yùn)行module:assembleRelease命令就可以了。這里簡單起見沒有進(jìn)行版本管理,大家如果需要自己加上就好了。值得注意的是,發(fā)布組件是唯一需要修改isRunAlone=false的情況,即使后面將組件集成到app中,也不需要修改isRunAlone的值,既保持isRunAlone=true即可。所以實(shí)際上在Androidstudio中,是可以看到三個(gè)application工程的,隨便點(diǎn)擊一個(gè)都是可以獨(dú)立運(yùn)行的,并且可以根據(jù)配置引入其他需要依賴的組件。這背后的工作都由com.dd.comgradle插件來默默完成。

項(xiàng)目中有三個(gè)application工程.png

2 組件交互

在這里組件的交互專指組件之間的數(shù)據(jù)傳輸,在我們的方案中使用的是接口+實(shí)現(xiàn)的方式,組件之間完全面向接口編程。

在demo中我們讓reader提供一個(gè)fragment給app使用來說明。首先reader組件在componentservice中定義自己的服務(wù)

publicinterfaceReadBookService{FragmentgetReadBookFragment();}

然后在自己的組件工程中,提供具體的實(shí)現(xiàn)類ReadBookServiceImpl:

publicclassReadBookServiceImplimplementsReadBookService{@OverridepublicFragmentgetReadBookFragment(){returnnewReaderFragment();? ? }}

提供了具體的實(shí)現(xiàn)類之后,需要在組件加載的時(shí)候把實(shí)現(xiàn)類注冊到Router中,具體的代碼在ReaderAppLike中,ReaderAppLike相當(dāng)于組件的application類,這里定義了onCreate和onStop兩個(gè)生命周期方法,對應(yīng)組件的加載和卸載。

publicclassReaderAppLikeimplementsIApplicationLike{? ? Router router = Router.getInstance();@OverridepublicvoidonCreate(){? ? ? ? router.addService(ReadBookService.class.getSimpleName(),newReadBookServiceImpl());? ? }@OverridepublicvoidonStop(){? ? ? ? router.removeService(ReadBookService.class.getSimpleName());? ? }}

在app中如何使用如reader組件提供的ReaderFragment呢?注意此處app是看不到組件的任何實(shí)現(xiàn)類的,它只能看到componentservice中定義的ReadBookService,所以只能面向ReadBookService來編程。具體的實(shí)例代碼如下:

Router router = Router.getInstance();if(router.getService(ReadBookService.class.getSimpleName()) !=null) {? ? ReadBookService service = (ReadBookService) router.getService(ReadBookService.class.getSimpleName());? ? fragment = service.getReadBookFragment();? ? ft = getSupportFragmentManager().beginTransaction();? ? ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();}

這里需要注意的是由于組件是可以動態(tài)加載和卸載的,因此在使用ReadBookService的需要進(jìn)行判空處理。我們看到數(shù)據(jù)的傳輸是通過一個(gè)中央路由Router來實(shí)現(xiàn)的,這個(gè)Router的實(shí)現(xiàn)其實(shí)很簡單,其本質(zhì)就是一個(gè)HashMap,具體代碼大家參見源碼。

通過上面幾個(gè)步驟就可以輕松實(shí)現(xiàn)組件之間的交互,由于是面向接口,所以組件之間是完全解耦的。至于如何讓組件之間在編譯階段不不可見,是通過上文所說的com.dd.comgradle實(shí)現(xiàn)的,這個(gè)在第一篇文章中已經(jīng)講到,后面會貼出具體的代碼。

3 UI跳轉(zhuǎn)

頁面(activity)的跳轉(zhuǎn)也是通過一個(gè)中央路由UIRouter來實(shí)現(xiàn),不同的是這里增加了一個(gè)優(yōu)先級的概念。具體的實(shí)現(xiàn)就不在這里贅述了,代碼還是很清晰的。

具體的功能介紹和使用規(guī)范,請大家參見文章:

android徹底組件化—UI跳轉(zhuǎn)升級改造

4 集成調(diào)試

集成調(diào)試可以認(rèn)為由app或者其他組件充當(dāng)host的角色,引入其他相關(guān)的組件一起參與編譯,從而測試整個(gè)交互流程。在demo中app和reader都可以充當(dāng)host的角色。在這里我們以app為例。

首先我們需要在根項(xiàng)目的gradle.properties中增加一個(gè)變量mainmodulename,其值就是工程中的主項(xiàng)目,這里是app。設(shè)置為mainmodulename的module,其isRunAlone永遠(yuǎn)是true。

然后在app項(xiàng)目的gradle.properties文件中增加兩個(gè)變量:

debugComponent=readercomponent,com.mrzhang.share:sharecomponent

compileComponent=readercomponent,sharecomponent

其中debugComponent是運(yùn)行debug的時(shí)候引入的組件,compileComponent是release模式下引入的組件。我們可以看到debugComponent引入的兩個(gè)組件寫法是不同的,這是因?yàn)榻M件引入支持兩種語法,module或者modulePackage:module,前者直接引用module工程,后者使用componentrelease中已經(jīng)發(fā)布的aar。

注意在集成調(diào)試中,要引入的reader和share組件是不需要把自己的isRunAlone修改為false的。我們知道一個(gè)application工程是不能直接引用(compile)另一個(gè)application工程的,所以如果app和組件都是isRunAlone=true的話在正常情況下是編譯不過的。秘密就在于com.dd.comgradle會自動識別當(dāng)前要調(diào)試的具體是哪個(gè)組件,然后把其他組件默默的修改為library工程,這個(gè)修改只在當(dāng)次編譯生效。

如何判斷當(dāng)前要運(yùn)行的是app還是哪個(gè)組件呢?這個(gè)是通過task來判斷的,判斷的規(guī)則如下:

assembleRelease → app

app:assembleRelease或者 :app:assembleRelease → app

sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→ sharecomponent

上面的內(nèi)容要實(shí)現(xiàn)的目的就是每個(gè)組件可以直接在Androidstudio中run,也可以使用命令進(jìn)行打包,這期間不需要修改任何配置,卻可以自動引入依賴的組件。這在開發(fā)中可以極大加快工作效率。

5 代碼邊界

至于依賴的組件是如何集成到host中的,其本質(zhì)還是直接使用compile project(...)或者compile modulePackage:module@aar。那么為啥不直接在build.gradle中直接引入呢,而要經(jīng)過com.dd.comgradle這個(gè)插件來進(jìn)行諸多復(fù)雜的操作?原因在第一篇文章中也講到了,那就是組件之間的完全隔離,也可以稱之為代碼邊界。如果我們直接compile組件,那么組件的所有實(shí)現(xiàn)類就完全暴露出來了,使用方就可以直接引入實(shí)現(xiàn)類來編程,從而繞過了面向接口編程的約束。這樣就完全失去了解耦的效果了,可謂前功盡棄。

那么如何解決這個(gè)問題呢?我們的解決方式還是從分析task入手,只有在assemble任務(wù)的時(shí)候才進(jìn)行compile引入。這樣在代碼的開發(fā)期間,組件是完全不可見的,因此就杜絕了犯錯(cuò)誤的機(jī)會。具體的代碼如下:

/** * 自動添加依賴,只在運(yùn)行assemble任務(wù)的才會添加依賴,因此在開發(fā)期間組件之間是完全感知不到的,這是做到完全隔離的關(guān)鍵 * 支持兩種語法:module或者modulePackage:module,前者之間引用module工程,后者使用componentrelease中已經(jīng)發(fā)布的aar *@paramassembleTask *@paramproject */privatevoidcompileComponents(AssembleTask assembleTask, Project project){? ? String components;if(assembleTask.isDebug) {? ? ? ? components = (String) project.properties.get("debugComponent")? ? }else{? ? ? ? components = (String) project.properties.get("compileComponent")? ? }if(components ==null|| components.length() ==0) {return;? ? }? ? String[] compileComponents = components.split(",")if(compileComponents ==null|| compileComponents.length ==0) {return;? ? }for(String str : compileComponents) {if(str.contains(":")) {? ? ? ? ? ? File file = project.file("../componentrelease/"+ str.split(":")[1] +"-release.aar")if(file.exists()) {? ? ? ? ? ? ? ? project.dependencies.add("compile", str +"-release@aar")? ? ? ? ? ? }else{thrownewRuntimeException(str +" not found ! maybe you should generate a new one ")? ? ? ? ? ? }? ? ? ? }else{? ? ? ? ? ? project.dependencies.add("compile", project.project(':'+ str))? ? ? ? }? ? }}

6 生命周期

在上一篇文章中我們就講過,組件化和插件化的唯一區(qū)別是組件化不能動態(tài)的添加和修改組件,但是對于已經(jīng)參與編譯的組件是可以動態(tài)的加載和卸載的,甚至是降維的。

首先我們看組件的加載,使用章節(jié)5中的集成調(diào)試,可以在打包的時(shí)候把依賴的組件參與編譯,此時(shí)你反編譯apk的代碼會看到各個(gè)組件的代碼和資源都已經(jīng)包含在包里面。但是由于每個(gè)組件的唯一入口ApplicationLike還沒有執(zhí)行oncreate()方法,所以組件并沒有把自己的服務(wù)注冊到中央路由,因此組件實(shí)際上是不可達(dá)的。

在什么時(shí)機(jī)加載組件以及如何加載組件?目前com.dd.comgradle提供了兩種方式,字節(jié)碼插入和反射調(diào)用。

字節(jié)碼插入模式是在dex生成之前,掃描所有的ApplicationLike類(其有一個(gè)共同的父類),然后通過javassist在主項(xiàng)目的Application.onCreate()中插入調(diào)用ApplicationLike.onCreate()的代碼。這樣就相當(dāng)于每個(gè)組件在application啟動的時(shí)候就加載起來了。

反射調(diào)用的方式是手動在Application.onCreate()中或者在其他合適的時(shí)機(jī)手動通過反射的方式來調(diào)用ApplicationLike.onCreate()。之所以提供這種方式原因有兩個(gè):對代碼進(jìn)行掃描和插入會增加編譯的時(shí)間,特別在debug的時(shí)候會影響效率,并且這種模式對Instant Run支持不好;另一個(gè)原因是可以更靈活的控制加載或者卸載時(shí)機(jī)。

這兩種模式的配置是通過配置com.dd.comgradle的Extension來實(shí)現(xiàn)的,下面是字節(jié)碼插入的模式下的配置格式,添加applicationName的目的是加快定位Application的速度。

combuild {? ? applicationName ='com.mrzhang.component.application.AppApplication'isRegisterCompoAuto =true}

demo中也給出了通過反射來加載和卸載組件的實(shí)例,在APP的首頁有兩個(gè)按鈕,一個(gè)是加載分享組件,另一個(gè)是卸載分享組件,在運(yùn)行時(shí)可以任意的點(diǎn)擊按鈕從而加載或卸載組件,具體效果大家可以運(yùn)行demo查看。

加載和卸載示例.png

二、組件化拆分的感悟

在最近兩個(gè)月的組件化拆分中,終于體會到了做到剝絲抽繭是多么艱難的事情。確定一個(gè)方案固然重要,更重要的是克服重重困難堅(jiān)定的實(shí)施下去。在拆分中,組件化方案也不斷的微調(diào),到現(xiàn)在終于可以欣慰的說,這個(gè)方案是經(jīng)歷過考驗(yàn)的,第一它學(xué)習(xí)成本比較低,組內(nèi)同事可以快速的入手,第二它效果明顯,得到本來run一次需要8到10分鐘時(shí)間(不過后面換了頂配mac,速度提升了很多),現(xiàn)在單個(gè)組件可以做到1分鐘左右。最主要的是代碼結(jié)構(gòu)清晰了很多,這位后期的并行開發(fā)和插件化奠定了堅(jiān)實(shí)的基礎(chǔ)。

總之,如果你面前也是一個(gè)龐大的工程,建議你使用該方案,以最小的代價(jià)盡快開始實(shí)施組件化。如果你現(xiàn)在負(fù)責(zé)的是一個(gè)開發(fā)初期的項(xiàng)目,代碼量還不大,那么也建議盡快進(jìn)行組件化的規(guī)劃,不要給未來的自己增加徒勞的工作量。

本文提出的Android組件化方案已經(jīng)開源,參見Android組件化方案開源項(xiàng)目

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評論 25 709
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,171評論 3 119
  • 平凡的一天,不平凡的人生,年輕就要躁起來!
    Fineyoga文龍閱讀 62評論 0 0
  • 他和她是初中同學(xué),在面臨高考的初三。大大咧咧的女孩總是一副沒心沒肺的樣子,成績平平。男孩成績很好,名列前茅,但卻...
    b64b13668898閱讀 405評論 0 0
  • 2018年1 月31日,那晚有月食發(fā)生,是超級月亮、“ 藍(lán)月亮 ” 和月全食血月同時(shí)出現(xiàn)的超級月食,是152年一遇...
    仕女簪花閱讀 395評論 0 1

友情鏈接更多精彩內(nèi)容