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

得到Android組件化方案已經(jīng)開源绿满,參見Android組件化方案開源。方案的解讀文章是一個小的系列窟扑,這是系列的第二篇文章:
1喇颁、Android徹底組件化方案實(shí)踐
2、Android徹底組件化demo發(fā)布
3嚎货、Android徹底組件化-代碼和資源隔離
4橘霎、Android徹底組件化—UI跳轉(zhuǎn)升級改造
5、Android徹底組件化—如何使用Arouter

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

一、JIMU使用指南

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

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

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

  • app是主項(xiàng)目,負(fù)責(zé)集成眾多組件寓调,控制組件的生命周期
  • reader和share是我們拆分的兩個組件
  • componentservice中定義了所有的組件提供的服務(wù)
  • basicres定義了全局通用的theme和color等公共資源
  • basiclib中是公共的基礎(chǔ)庫锌唾,一些第三方的庫(okhttp等)也統(tǒng)一交給basiclib來引入

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

我們在demo中要實(shí)現(xiàn)的場景是:主項(xiàng)目app集成reader和share兩個組件载萌,其中reader提供一個讀書的fragment給app調(diào)用(組件交互)惧财,share提供一個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è)置一個isRunAlone的變量來區(qū)分不同的場景,唯一的不同點(diǎn)是在組件的build.gradle中不需要寫下面的樣板代碼:

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

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

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

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

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

2 組件交互

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

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

public interface ReadBookService {
    Fragment getReadBookFragment();
}

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

public class ReadBookServiceImpl implements ReadBookService {
    @Override
    public Fragment getReadBookFragment() {
        return new ReaderFragment();
    }
}

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

public class ReaderAppLike implements IApplicationLike {
    Router router = Router.getInstance();
    @Override
    public void onCreate() {
        router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
    }
    @Override
    public void onStop() {
        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ù)的傳輸是通過一個中央路由Router來實(shí)現(xiàn)的筐咧,這個Router的實(shí)現(xiàn)其實(shí)很簡單鸯旁,其本質(zhì)就是一個HashMap,具體代碼大家參見源碼量蕊。

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

3 UI跳轉(zhuǎn)

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

具體的功能介紹和使用規(guī)范舅锄,請大家參見文章:
android徹底組件化—UI跳轉(zhuǎn)升級改造

4 集成調(diào)試

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

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

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

debugComponent=readercomponent,com.mrzhang.share:sharecomponent
compileComponent=readercomponent,sharecomponent

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

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

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

  • assembleRelease → app
  • app:assembleRelease或者 :app:assembleRelease → app
  • sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→ sharecomponent

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

5 代碼邊界

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

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

  /**
 * 自動添加依賴活合,只在運(yùn)行assemble任務(wù)的才會添加依賴雏婶,因此在開發(fā)期間組件之間是完全感知不到的,這是做到完全隔離的關(guān)鍵
 * 支持兩種語法:module或者modulePackage:module,前者之間引用module工程白指,后者使用componentrelease中已經(jīng)發(fā)布的aar
 * @param assembleTask
 * @param project
 */
private void compileComponents(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 {
                throw new RuntimeException(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)試,可以在打包的時候把依賴的組件參與編譯赋焕,此時你反編譯apk的代碼會看到各個組件的代碼和資源都已經(jīng)包含在包里面参歹。但是由于每個組件的唯一入口ApplicationLike還沒有執(zhí)行oncreate()方法,所以組件并沒有把自己的服務(wù)注冊到中央路由宏邮,因此組件實(shí)際上是不可達(dá)的泽示。

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

  • 字節(jié)碼插入模式是在dex生成之前,掃描所有的ApplicationLike類(其有一個共同的父類)飒炎,然后通過javassist在主項(xiàng)目的Application.onCreate()中插入調(diào)用ApplicationLike.onCreate()的代碼埋哟。這樣就相當(dāng)于每個組件在application啟動的時候就加載起來了。
  • 反射調(diào)用的方式是手動在Application.onCreate()中或者在其他合適的時機(jī)手動通過反射的方式來調(diào)用ApplicationLike.onCreate()郎汪。之所以提供這種方式原因有兩個:對代碼進(jìn)行掃描和插入會增加編譯的時間赤赊,特別在debug的時候會影響效率,并且這種模式對Instant Run支持不好煞赢;另一個原因是可以更靈活的控制加載或者卸載時機(jī)抛计。

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

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

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

加載和卸載示例.png

二、組件化拆分的感悟

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

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

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

JIMU的討論群色乾,群號693097923,歡迎大家加入:


進(jìn)群請掃碼
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末领突,一起剝皮案震驚了整個濱河市暖璧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌君旦,老刑警劉巖澎办,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異于宙,居然都是意外死亡浮驳,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門捞魁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來至会,“玉大人,你說我怎么就攤上這事谱俭》罴” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵昆著,是天一觀的道長县貌。 經(jīng)常有香客問我,道長凑懂,這世上最難降的妖魔是什么煤痕? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上摆碉,老公的妹妹穿的比我還像新娘塘匣。我一直安慰自己,他們只是感情好巷帝,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布忌卤。 她就那樣靜靜地躺著,像睡著了一般楞泼。 火紅的嫁衣襯著肌膚如雪驰徊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天堕阔,我揣著相機(jī)與錄音棍厂,去河邊找鬼。 笑死印蔬,一個胖子當(dāng)著我的面吹牛勋桶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侥猬,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼例驹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了退唠?” 一聲冷哼從身側(cè)響起鹃锈,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瞧预,沒想到半個月后屎债,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垢油,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年盆驹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滩愁。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡躯喇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出硝枉,到底是詐尸還是另有隱情廉丽,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布妻味,位于F島的核電站正压,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏责球。R本人自食惡果不足惜焦履,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一拓劝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧裁良,春花似錦凿将、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽笛匙。三九已至侨把,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妹孙,已是汗流浹背秋柄。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蠢正,地道東北人骇笔。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像嚣崭,于是被迫代替她去往敵國和親笨触。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容