組件化調(diào)研與實(shí)踐

what细溅?什么是組件化?

簡介

客戶端(架構(gòu))yanlu的老文章

Android組件化開發(fā)實(shí)踐和案例分享(方案1)

Android 組件化最佳實(shí)踐(方案1)

Android組件化開發(fā)思想與實(shí)踐(方案1)

回歸初心:極簡 Android 組件化方案(方案2:推薦)

可以閱讀以上三篇文章儡嘶,了解組件化的基礎(chǔ)概念喇聊。

實(shí)現(xiàn)組件化目前有2套主要的方案:(1)改造module,支持library和application兩種運(yùn)行模式蹦狂,開發(fā)過程中進(jìn)行手動切換誓篱。(2)為每個module新增一個專門用于獨(dú)立運(yùn)行的AppModule,module無需改動凯楔,跑整包或組件包燕鸽,運(yùn)行不同的AppModule即可;基于開閉原則啼辣,及低侵入性啊研,操作便捷性等優(yōu)點(diǎn),推薦使用方案2

演示

??SVID_20200929_102256_1.mp4

將多鹿app中test相關(guān)的代碼抽取成獨(dú)立module后鸥拧,即可獨(dú)立運(yùn)行党远。

why?為什么需要組件化富弦?組件化有什么優(yōu)點(diǎn)沟娱?

組件獨(dú)立打包運(yùn)行,提升開發(fā)效率(提升10倍左右編譯效率):

這是我們平常的開發(fā)模式腕柜,當(dāng)我們有新業(yè)務(wù)時济似,會創(chuàng)建一個新的module A矫废,或者直接在module app中開發(fā),每次修改一行Java代碼砰蠢,進(jìn)行的增量編譯時間大概是50秒(自己電腦親測)蓖扑。為什么這么慢?因為這時候打的是整包台舱,每次編譯都會觸發(fā)app里所有代碼的javac律杠,dexMerge,packageApk等竞惋,然后去編譯一個完整的多鹿apk柜去。假設(shè)我只是修改Module A的一行代碼,需要去編譯APP的所有代碼嗎拆宛?如何只編譯Module A中我修改的代碼呢嗓奢?答案是:跳過APP,只打Module A的組件包浑厚。

如圖:為Module A單獨(dú)創(chuàng)建一個Module A App(這是一個Application)股耽,包含了Module A運(yùn)行所需要的必要依賴library、biz瞻颂、api和其它三方SDK豺谈,如:Arouter,Retrofit等(不需要像APP那樣依賴眾多的三方SDK)贡这。此時單獨(dú)運(yùn)行Module A App茬末,修改一行Java代碼的增量編譯時間僅僅只要5秒左右(原因可想而知:1.擺脫了APP,此時moduleA中的業(yè)務(wù)代碼極少盖矫;2.ModuleA移除了不必要的三方SDK和依賴丽惭,只有少量,必要的SDK)辈双。當(dāng)然责掏,如果這個module A要依賴更多的三方SDK,編譯速度也會有所下降湃望,所以要盡量保持組件module的最小化换衬,才能實(shí)現(xiàn)編譯效率的最大提升。開發(fā)階段证芭,通過Module A APP運(yùn)行組件包瞳浦,調(diào)試代碼;集成階段废士,通過Module APP運(yùn)行整包叫潦,完善與其他組件的聯(lián)調(diào),Module A則還是一個獨(dú)立的Library官硝,除了業(yè)務(wù)代碼矗蕊,無需其他任何改動短蜕。

動態(tài)化/插件化

組件化的最終目的是為了實(shí)現(xiàn)插件化,在運(yùn)行期間動態(tài)的替換傻咖,更新組件朋魔,實(shí)現(xiàn)業(yè)務(wù)功能的動態(tài)化。

高內(nèi)聚没龙,低耦合

如果不進(jìn)行組件化拆分铺厨,隨著APP代碼的增多缎玫,編譯會越來越慢

減少代碼耦合硬纤,提高代碼復(fù)用率

module拆分出來后,可以更方便的進(jìn)行單元測試赃磨,A/B Test等

組件微服務(wù)化筝家,更利于跨部門協(xié)作。大團(tuán)隊中邻辉,不同部門/團(tuán)隊負(fù)責(zé)不同組件溪王,發(fā)版前集成并編譯整包。小團(tuán)隊中值骇,如果可以將代碼打散成一個個module莹菱,每個人負(fù)責(zé)1-2個組件,各個模塊之間根據(jù)api層進(jìn)行解耦吱瘩,就可以最大限度的減少沖突道伟,實(shí)現(xiàn)更高效的并行開發(fā)。

編輯APK時使碾,可以根據(jù)BC端蜜徽,依賴不同的module,只將BC端中用到的代碼打進(jìn)apk票摇,可以優(yōu)化APK大小拘鞋。

基于多鹿/多鹿老師APP,可以拆出哪些組件矢门?

業(yè)務(wù)為導(dǎo)向進(jìn)行拆分的module盆色,稱謂模塊,可實(shí)現(xiàn)C/B端的復(fù)用祟剔。例如:首頁模塊隔躲,動態(tài)列表模塊,消息模塊峡扩,我的模塊蹭越,動態(tài)發(fā)布模塊,寶寶模塊教届,班級模塊响鹃,學(xué)校模塊驾霜,直播模塊,成長冊模塊买置,小目標(biāo)模塊...等等

技術(shù)為導(dǎo)向進(jìn)行拆分的module粪糙,稱謂組件,可實(shí)現(xiàn)不同模塊之間的復(fù)用忿项。例如:登錄組件蓉冈,播放器組件,支付組件轩触,分享組件寞酿,IM組件,短視頻處理組件...等等

總結(jié)

1.能立馬見到的收益:組件獨(dú)立運(yùn)行脱柱,提升10倍編譯效率伐弹。

2.長期收益:代碼架構(gòu)的健壯性,復(fù)用性榨为,和可擴(kuò)展性惨好。

how?如何創(chuàng)建出一個可以獨(dú)立運(yùn)行起來的組件随闺?

module接口層?

如圖日川,這是我們將要采用的方案2的組件開發(fā)架構(gòu)。但是還存在一個問題矩乐,就是不同module的循環(huán)依賴龄句。

假設(shè)moduleA可能依賴moduleB的某些方法,同時moduleB也會依賴moduleA某些方法的情況绰精,這樣會產(chǎn)生循環(huán)依賴撒璧,是不被允許的。解決方案:為A和B分別創(chuàng)建Module A-I笨使,Module B-I接口層卿樱,將A、B對外開放的model硫椰,interface繁调,或是event都放在接口層,A靶草、B分別實(shí)現(xiàn)A-I和B-I的接口協(xié)議蹄胰,彼此module之間不依賴,且不應(yīng)該知道彼此的實(shí)現(xiàn)細(xì)節(jié)奕翔,如下圖裕寨。

所以假設(shè)現(xiàn)在有個module A,要實(shí)現(xiàn)組件化拆分,就需要為這個module分別創(chuàng)建module A-I(對外暴露接口)宾袜,module A-App(獨(dú)立運(yùn)行組件)兩個module捻艳,APP應(yīng)該只依賴module A-I里面的接口,不該使用Module A的實(shí)現(xiàn)類庆猫。

多鹿APP基于組件化拆分的示例模板

標(biāo)準(zhǔn)的組件化module模板

module_template_api

ModuleTemplateApiEvent:組件對外暴露的event认轨,盡量把和這個組件相關(guān)的event放在module_api中,而不是api月培,保證內(nèi)聚嘁字,app或者其他module可以依賴這個組件的module_api。

public class ModuleTemplateApiEvent {

? ? public String status;

? ? public ModuleTemplateApiEvent(String status) {

? ? ? ? this.status = status;

? ? }

}

ModuleTemplateApiModel:組件對外暴露的model杉畜。

public class ModuleTemplateApiModel extends BaseReqModel {

? ? public String id;

? ? public String name;

}

ModuleTemplateApiScheme:組件對外暴露的scheme協(xié)議纪蜒。

/**

* 組件scheme協(xié)議

* @author listen

*/

public class ModuleTemplateApiScheme {

? ? public static final String MODULE_MAIN = "/module_template_api/main";

? ? public static final String MODULE_SERVICE_NAME = "/module_template_api/service";

}

ModuleTemplateApiService:組件對外暴露的接口協(xié)議(這里使用Arouter Service實(shí)現(xiàn))。

public interface ModuleTemplateApiService extends IProvider {

? ? /**

? ? * 獲取moduleName

? ? * @return moduleName

? ? */

? ? String getModuleName();

}

module_template

Constanst:常量

ModuleTemplateActivity:組件中示例代碼寻行,如網(wǎng)絡(luò)請求霍掺,發(fā)送消息匾荆,跳轉(zhuǎn)H5拌蜘,F(xiàn)lutter等

ModuleTemplateApplication:組件的內(nèi)部私有的初始化邏輯,假設(shè)當(dāng)前是直播組件牙丽,則這里就是zegoSDK的初始化邏輯简卧,保證直播相關(guān)邏輯內(nèi)聚在這個module中。

/**

* 當(dāng)前業(yè)務(wù)module內(nèi)部私有的初始化邏輯

*/

public class ModuleTemplateApplication extends Application {

? ? private static final String TAG = "ModuleTemplateApplication";

? ? private static Context sApplication;

? ? public static Context getInstance() {

? ? ? ? return sApplication;

? ? }

? ? @SuppressLint("LongLogTag")

? ? public ModuleTemplateApplication(Context application) {

? ? ? ? Log.d(TAG, "init");

? ? ? ? sApplication = application;

? ? }

}

ModuleTemplateServiceImpl:ModuleTemplateService的實(shí)現(xiàn)類烤芦,具體的邏輯在此處實(shí)現(xiàn)举娩,外部module依賴ModuleTemplateService,而不是ModuleTemplateServiceImpl

@Route(path = ModuleTemplateApiScheme.MODULE_SERVICE_NAME)

public class ModuleTemplateServiceImpl implements ModuleTemplateApiService {

? ? @Override

? ? public String getModuleName() {

? ? ? ? return Constants.MODULE_NAME;

? ? }

? ? @SuppressLint("LongLogTag")

? ? @Override

? ? public void init(Context context) {

? ? ? ? // 第一次ARouter.navigation()調(diào)用的時候构罗,會執(zhí)行init方法铜涉,且只會調(diào)用一次,可以在這里觸發(fā)module內(nèi)部的初始化邏輯

? ? ? ? Log.e("ModuleTemplateServiceImpl", "init=" + context);

? ? ? ? new ModuleTemplateApplication(context);

? ? }

}

module_template_run

MainActivity:歡迎頁遂唧,簡單的組件頁面跳轉(zhuǎn)

ModuleApplication:當(dāng)我們獨(dú)立運(yùn)行組件時芙代,需要一些網(wǎng)絡(luò)庫,圖片庫一些最基礎(chǔ)的初始化時盖彭,就可以在這里實(shí)現(xiàn)纹烹,簡單理解就是AndroidApp里面做的事情,需要搬到這里來≌俦撸現(xiàn)在里面只有Arouter和網(wǎng)絡(luò)庫的初始化邏輯铺呵,如果需要拆分IM或是直播組件,則這里初始化的SDK就需要增加了隧熙。

public class ModuleApplication extends ApiApplication {

? ? @Override

? ? protected void attachBaseContext(Context base) {

? ? ? ? super.attachBaseContext(base);

? ? ? ? MultiDex.install(this);

? ? ? ? EnvConfig.init(isDebug, 1, "1.0", "Android");

? ? ? ? AppInit.beforeInit(BuildConfig.DEBUG);

? ? }

? ? @Override

? ? public void onCreate() {

? ? ? ? super.onCreate();

? ? ? ? // 盡可能早片挂,推薦在Application中初始化

? ? ? ? ARouter.init(this);

? ? ? ? AppInit.onInit(BuildConfig.DEBUG);

? ? ? ? // 初始化ModuleTemplateRun組件

? ? ? ? ARouter.getInstance().build(ModuleTemplateApiScheme.MODULE_SERVICE_NAME).navigation();

? ? }

}

module創(chuàng)建之一鍵生成

在MaltBaby-Android的根目錄下,我寫了一個shell腳本"moduleCreate.sh",只要在根目錄下輸入:"./moduleCreate.sh module_a"音念,就會根據(jù)module_template的項目結(jié)構(gòu)滋将,創(chuàng)建出module_a,module_a_api症昏,module_a_run三個標(biāo)準(zhǔn)的組件化module随闽。同步下build.gradle文件后,就會在app執(zhí)行框中看到module_a_run肝谭,直接運(yùn)行即可掘宪。

問題

組件整理、抽取

新module創(chuàng)建:如果當(dāng)前需要創(chuàng)建的module是新的功能攘烛,則可以根據(jù)module_template直接copy即可魏滚。

老module抽取:如果當(dāng)前需要從app中拆分老的代碼成為module坟漱,則成本較高鼠次,需要將module依賴的所有類,xml芋齿,sdk抽取到module中腥寇,并把一些app中用到公用代碼,提取到library中觅捆,然后定義好api層赦役,供外部調(diào)用。

組件間的交互和通信

頁面跳轉(zhuǎn):使用Arouter進(jìn)行schme跳轉(zhuǎn)栅炒。頁面跳轉(zhuǎn)時掂摔,統(tǒng)一使用scheme還有個好處,就是未來可以進(jìn)行Native到H5的頁面降級赢赊。

方法調(diào)用(有入?yún)?出參):要調(diào)用別的Module的某個方法乙漓,并希望有返回值時,則使用Arouter Service實(shí)現(xiàn)释移,其實(shí)就是定義一個接口叭披,面向抽象編程。

單向通信(只有入?yún)ⅲ喝绻皇菃蜗虻南騽e的module發(fā)個消息秀鞭,或通知趋观,使用EventBus即可

數(shù)據(jù)傳遞:sp,sqlite锋边。比如登錄組件中皱坛,登錄成功后將UserInfo保存在sp中,別的module從sp中直接獲取即可豆巨。

組件的隔離(代碼和資源)

代碼隔離(推薦runtimeOnly依賴剩辟,不過datdbinding的編譯會失敗,所以先用implement):組件包括module和module_api兩個module,理論上module_api是接口抽象層贩猎,module是基于module_api層的具體實(shí)現(xiàn)類熊户,組件間相互依賴的時候,不該暴露太多細(xì)節(jié)吭服,只把需要讓外界知道的通過api層對外暴露即可嚷堡。這樣做是為了避免組件間產(chǎn)生不必要的耦合,畢竟組件化的收益之一就是降低耦合艇棕。組件代碼相互隔離的較好的情況下蝌戒,未來替換或更新組件才能成為可能。例如:播放器組件沼琉,只在api層對外暴露北苟,播放,暫停等常用接口打瘪,至于內(nèi)部實(shí)現(xiàn)是阿里云友鼻,還是七牛,外界不需要知道闺骚,就可以更方便的實(shí)現(xiàn)組件替換彩扔。

資源隔離:不同module的資源可能重命,可以通過resourcePrefix葛碧,給每個module添加資源前綴的校驗借杰,如果某個jpg,或是xml資源进泼,沒有按照這個前綴規(guī)則命名,就會報紅色警告纤虽。

TODO?

將app中的業(yè)務(wù)代碼拆分成module乳绕,最后app應(yīng)該只是個殼工程,負(fù)責(zé)將不同module引入并串聯(lián)起來

將所有module上傳到maven逼纸,不同module之間依賴抽象api洋措,不依賴具體實(shí)現(xiàn),每個module支持版本升級杰刽,降級菠发。

每個組件module單獨(dú)編譯運(yùn)行,若依賴其他若干module贺嫂,可以按需引入滓鸠,若干個組件module組合后,編譯運(yùn)行第喳。

B/C端APK打包時糜俗,只依賴各自業(yè)務(wù)module

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悠抹,更是在濱河造成了極大的恐慌珠月,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件楔敌,死亡現(xiàn)場離奇詭異啤挎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)卵凑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門侵浸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人氛谜,你說我怎么就攤上這事掏觉。” “怎么了值漫?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵澳腹,是天一觀的道長。 經(jīng)常有香客問我杨何,道長酱塔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任危虱,我火速辦了婚禮羊娃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘埃跷。我一直安慰自己蕊玷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布弥雹。 她就那樣靜靜地躺著垃帅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪剪勿。 梳的紋絲不亂的頭發(fā)上贸诚,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機(jī)與錄音厕吉,去河邊找鬼酱固。 笑死,一個胖子當(dāng)著我的面吹牛头朱,可吹牛的內(nèi)容都是我干的运悲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼髓窜,長吁一口氣:“原來是場噩夢啊……” “哼扇苞!你這毒婦竟也來了欺殿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤鳖敷,失蹤者是張志新(化名)和其女友劉穎脖苏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體定踱,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡棍潘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了崖媚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亦歉。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖畅哑,靈堂內(nèi)的尸體忽然破棺而出肴楷,到底是詐尸還是另有隱情,我是刑警寧澤荠呐,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布慰毅,位于F島的核電站褥符,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哲嘲。R本人自食惡果不足惜呵哨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一引几、第九天 我趴在偏房一處隱蔽的房頂上張望丹允。 院中可真熱鬧航瞭,春花似錦、人聲如沸钞钙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽歇竟。三九已至挥唠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焕议,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工弧关, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盅安,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓世囊,卻偏偏與公主長得像别瞭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子株憾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

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