Android 組件化多module依賴優(yōu)雅方案(建議收藏)
作者:
leobert-lan, 鏈接:https://juejin.cn/post/6925629544946892813
背景
如果沒有記錯(cuò),15年那會(huì)Android項(xiàng)目逐步轉(zhuǎn)向使用Gradle構(gòu)建,時(shí)至今日,組件化已經(jīng)不再是一個(gè)新穎的話題。
雖然我將這篇文章放在了Gradle分類中,但是我們知道,使用gradle構(gòu)建的后端項(xiàng)目, 熱點(diǎn)聚焦在:實(shí)現(xiàn)微服務(wù)化,項(xiàng)目是拆開的,決定了依賴庫已經(jīng)是靜態(tài)jar包,和我們 要討論的場景是不一致的。所以我們還是在Android領(lǐng)域中討論這個(gè)問題。
在各種方案的組件化實(shí)施中,一定會(huì)將部分功能模塊拆分,進(jìn)行l(wèi)ibrary下沉。于是,就有了處理依賴的場景。相信大家思考過這樣一個(gè)問題:如果下沉的library也提前編譯好靜態(tài)aar包,我們的項(xiàng)目編譯時(shí)間會(huì)縮短。
毋庸置疑,這樣做會(huì)直接從源頭解決 編譯時(shí)間長的問題,就是減少編譯內(nèi)容。但是,項(xiàng)目合并在一起,難免就想在開發(fā)下層library時(shí),直接用上層業(yè)務(wù)集成進(jìn)行冒煙。ps:這個(gè)做法并不好,應(yīng)當(dāng)為library配置好冒煙測試環(huán)境,雖然會(huì)耗費(fèi)掉一定的時(shí)間。
理想歸理想,最終還是會(huì)敗給現(xiàn)實(shí),這個(gè)問題就變成了魚和熊掌想要兼得的問題。
為了讓閱讀的目標(biāo)更加明確,我們先思考一個(gè)問題:

這樣一個(gè)項(xiàng)目依賴關(guān)系,如果做到改動(dòng)B 的內(nèi)容,卻不需要重新編譯A,運(yùn)行APP,驗(yàn)證B的修改 我們下面會(huì)進(jìn)行一定地展開,來體悟這個(gè)問題。
為什么使用遠(yuǎn)程倉庫中的依賴包比使用本地靜態(tài)aar要方便
我們知道,對于一個(gè)module,我們對其進(jìn)行編譯生成靜態(tài)aar包,只會(huì)處理它自身的內(nèi)容。那么他的依賴是如何傳遞的?
通過pom文件
舉個(gè)例子:
我們新建一個(gè)module,看一下依賴:
dependencies?{
????implementation?"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
????implementation?'androidx.core:core-ktx:1.3.2'
????implementation?'androidx.appcompat:appcompat:1.2.0'
????implementation?'com.google.android.material:material:1.2.1'
????testImplementation?'junit:junit:4.+'
????androidTestImplementation?'androidx.test.ext:junit:1.1.2'
????androidTestImplementation?'androidx.test.espresso:espresso-core:3.3.0'
}
利用maven plugin?進(jìn)行發(fā)布,會(huì)有任務(wù)生成pom文件,如下:
<project?xsi:schemaLocation="http://maven.apache.org/POM/4.0.0?http://maven.apache.org/xsd/maven-4.0.0.xsd"?xmlns="http://maven.apache.org/POM/4.0.0"
????xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
??<modelVersion>4.0.0modelVersion>
??<groupId>leobertgroupId>
??<artifactId>BartifactId>
??<version>1.0.0version>
??<packaging>aarpackaging>
??<dependencies>
????<dependency>
??????<groupId>org.jetbrains.kotlingroupId>
??????<artifactId>kotlin-stdlibartifactId>
??????<version>1.4.21version>
??????<scope>compilescope>
????dependency>
????<dependency>
??????<groupId>androidx.coregroupId>
??????<artifactId>core-ktxartifactId>
??????<version>1.3.2version>
??????<scope>compilescope>
????dependency>
????<dependency>
??????<groupId>androidx.appcompatgroupId>
??????<artifactId>appcompatartifactId>
??????<version>1.2.0version>
??????<scope>compilescope>
????dependency>
????<dependency>
??????<groupId>com.google.android.materialgroupId>
??????<artifactId>materialartifactId>
??????<version>1.2.1version>
??????<scope>compilescope>
????dependency>
??dependencies>
project>
我們發(fā)現(xiàn),關(guān)于測試相關(guān)的依賴并沒有被收錄到pom文件中。這很合理,測試代碼是針對該module的,并不需要提供給使用方,其依賴自然也不需要傳遞。我們知道,AGP中現(xiàn)在有4種聲明依賴的方式(除去testXXX這種變種)
api implementation compileOnly runtimeOnly
runtimeOnly對應(yīng)以前的apk方式聲明依賴,我們直接忽略掉,測試一下生成的pom文件。
dependencies?{
????api?"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
????implementation?'androidx.core:core-ktx:1.3.2'
????compileOnly?'androidx.appcompat:appcompat:1.2.0'
????compileOnly?'com.google.android.material:material:1.2.1'
????testImplementation?'junit:junit:4.+'
????androidTestImplementation?'androidx.test.ext:junit:1.1.2'
????androidTestImplementation?'androidx.test.espresso:espresso-core:3.3.0'
}
<project?xsi:schemaLocation="http://maven.apache.org/POM/4.0.0?http://maven.apache.org/xsd/maven-4.0.0.xsd"?xmlns="http://maven.apache.org/POM/4.0.0"
????xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
??<modelVersion>4.0.0modelVersion>
??<groupId>leobertgroupId>
??<artifactId>BartifactId>
??<version>1.0.0version>
??<packaging>aarpackaging>
??<dependencies>
????<dependency>
??????<groupId>org.jetbrains.kotlingroupId>
??????<artifactId>kotlin-stdlibartifactId>
??????<version>1.4.21version>
??????<scope>compilescope>
????dependency>
????<dependency>
??????<groupId>androidx.coregroupId>
??????<artifactId>core-ktxartifactId>
??????<version>1.3.2version>
??????<scope>compilescope>
????dependency>
??dependencies>
project>
使用compileOnly方式的并沒有被收錄到pom文件中,而api和implementation?方式,在pom文件中,都表現(xiàn)為 采用compile的方案應(yīng)用依賴。
ps:
api和implementation在編碼期的不同,不是我們討論的重點(diǎn),略。
回到我們開始的問題,將library發(fā)布時(shí),按照約定,會(huì)將library本身的依賴收錄到pom文件中。相應(yīng)的,使用方使用 倉庫中的依賴項(xiàng)時(shí),gradle會(huì)拉取其對應(yīng)的pom文件,并添加依賴。
所以,如果我們直接使用一個(gè)編譯好的靜態(tài)包,而丟棄了他對應(yīng)的pom文件時(shí),可能會(huì)丟失依賴,出現(xiàn)打包失敗或者運(yùn)行異常。這意味著我們需要人為維護(hù)依賴傳遞
我們記住這些內(nèi)容,并先放到一邊。
下沉后,library會(huì)有多個(gè)層級
例如圖中:APP => A => B, 即APP依賴A,A依賴B,而A和B都是library
我們知道,對于B,并不會(huì)有什么說法,只會(huì)出現(xiàn)在A和APP
如果不使用靜態(tài)包,那么A會(huì)聲明:
api?project(':B')
//或者
implementation?project(':B')
我們先看一下,這樣生成的library-A的pom文件
<project?xsi:schemaLocation="http://maven.apache.org/POM/4.0.0?http://maven.apache.org/xsd/maven-4.0.0.xsd"?xmlns="http://maven.apache.org/POM/4.0.0"
????xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
??<modelVersion>4.0.0modelVersion>
??<groupId>leobertgroupId>
??<artifactId>AartifactId>
??<version>1.0.0version>
??<packaging>aarpackaging>
??<dependencies>
????<dependency>
??????<groupId>DemogroupId>
??????<artifactId>BartifactId>
??????<version>unspecifiedversion>
??????<scope>compilescope>
????dependency>
??dependencies>
project>
會(huì)得到groupID是項(xiàng)目名,artifactId是module名,version是未知的一個(gè)依賴項(xiàng)。假如我將A編譯為靜態(tài)包并發(fā)布到倉庫,并運(yùn)用了pom中的依賴描述,一定會(huì)得到無法找到:Demo-B-unspecified.pom?的問題。
當(dāng)然,這個(gè)問題可以通過在APP中重新聲明 B的依賴 來解決。
這意味著,我們需要時(shí)刻保持警惕,維護(hù)各個(gè)module的依賴。否則,我們無法同時(shí)享受:靜態(tài)包減少編譯 & 隨心的修改局部并集成測試
這顯然是一件不人道主義的事情。
反思一下,對于A而言,它需要B,但僅在兩個(gè)時(shí)機(jī)需要:
編譯時(shí)受檢,完成編譯 運(yùn)行時(shí)
作為一個(gè)library,它本身并不對應(yīng)運(yùn)行時(shí),所以,compileOnly?是其聲明對B的依賴的最佳方式。這意味著,最終對應(yīng)運(yùn)行時(shí) 的內(nèi)容,即APP,需要在編譯時(shí)加入 對B的依賴。在原先?A使用Api方式聲明對B的依賴時(shí),是通過gradle分析pom文件實(shí)現(xiàn)的依賴加入。而現(xiàn)在,需要人為維護(hù),只需要實(shí)現(xiàn) 人道主義,就可以魚和熊掌兼得。
反思依賴傳遞的本質(zhì)

一般我們會(huì)像下面的演示代碼一樣聲明依賴:
//APP:
implementation?project('A')
implementation?project('Foo')
//A:
implementation?project('B')
implementation?project('Bar')
因?yàn)橐蕾噦鬟f性,APP其實(shí)依賴了A,Foo,B,Bar。其實(shí)就是一顆樹中,除去根節(jié)點(diǎn)的節(jié)點(diǎn)集合。而對于一個(gè)非根節(jié)點(diǎn),它被依賴的形式只有兩種:
靜態(tài)包,不需要重新編譯,節(jié)約編譯時(shí)間 module,需要再次編譯,可以運(yùn)用最新改動(dòng)
我們可以定義這樣一個(gè)鍵值對信息:
project.ext.depRules?=?[
????????"B":?"p",
????????"A":?"a"
]
"p"代表使用project,"a"代表使用靜態(tài)包。
并將這顆樹的內(nèi)容表達(dá)出來:我們先忽略掉Foo和Bar
project.ext.deps?=?[
????????"A"??:?[
????????????????"B":?[
????????????????????????"p":?project(':B'),
????????????????????????"a":?'leobert:B:1.0.0'
????????????????]
????????],
????????"APP":?[
????????????????"A":?[
????????????????????????"p":?project(':A'),
????????????????????????"a":?'leobert:A:1.0.0'
????????????????]
????????]
].with(true)?{
????A.each?{?e?->
????????APP.put(e.key,?e.value)
????}
}
以A為例,我們可以通過代碼實(shí)現(xiàn)動(dòng)態(tài)添加依賴:
project.afterEvaluate?{?p?->
????????println("handle?deps?for:"?+?p)
????????deps.A.each?{?e?->
????????????def?rule?=?depRules.get(e.key)
????????????println("find?deps?of?A:?rule?is"?+?rule?+?"?,dep?is:"?+?e.value.get(rule).toString())
????????????project.dependencies.add("compileOnly",?e.value.get(rule))
????????}
????}
同理,對于APP:
project.afterEvaluate?{?p->
????????println("handle?deps?for:"?+?p)
????????deps.APP.each?{?e?->
????????????def?rule?=?depRules.get(e.key)
????????????println("find?deps?of?App:rule?is"?+?rule?+?"?,dep?is:"?+?e.value.get(rule).toString())
????????????project.dependencies.add("implementation",?e.value.get(rule))
????????}
????}
查看輸出:
Configure?project?:A
handle?deps?for:project?':A'
find?deps?of?A:?rule?isp?,dep?is:project?':B'
Configure?project?:app
handle?deps?for:project?':app'
find?deps?of?App:rule?isa?,dep?is:leobert:A:1.0.0
find?deps?of?App:rule?isp?,dep?is:project?':B'
這樣,我們就可以通過修改對應(yīng)節(jié)點(diǎn)的依賴方式配置而實(shí)現(xiàn)魚和熊掌兼得。不再受pom文件的約束。當(dāng)時(shí),我們回到上面說的不人道主義之處,我們通過了with?函數(shù),將A自身的依賴信息,注入到APP中。
但是當(dāng)樹的規(guī)模變大時(shí),人為維護(hù)就很累了。這是必須要解決的,當(dāng)然,這很容易解決。我們直接使用遞歸處理即可
貼近人的直觀感受才優(yōu)雅,逐步實(shí)現(xiàn)人道主義 我們添加一個(gè)全局閉包:
ext.utils?=?[
????????applyDependency:?{?project,?e?->
????????????def?rule?=?depRules.get(e.key)
????????????println("find?deps?of?App:rule?is?"?+?rule?+?"?,dep?is:"?+?e.value.get(rule).toString())
????????????project.dependencies.add("implementation",?e.value.get(rule))
????????????try?{
????????????????println("try?to?add?sub?deps?of:"?+?e.key)
????????????????def?sub?=?deps.get(e.key)
????????????????if?(sub?!=?null?&&?sub.get("isEnd")?!=?true)?{
????????????????????sub.each?{?se?->
????????????????????????ext.utils.applyDependency(project,?se)
????????????????????}
????????????????}
????????????}?catch?(Exception?ignore)?{
????????????}
????????}
]
注意,因?yàn)槲覀兌x的依賴信息是:moduleName-> (moduleName -> (scopeName-> depInfo))?的方式。
這導(dǎo)致我們判斷末端節(jié)點(diǎn)有一定的困難,即遞歸的尾部判斷存在困難,我們需要人為標(biāo)記一下末端節(jié)點(diǎn) 這時(shí),我們只需描述一下樹即可:同樣忽略Foo,Bar
project.ext.deps?=?[
????????"A"??:?[
????????????????"B":?[
????????????????????????"isEnd":?true,
????????????????????????"p"????:?project(':B'),
????????????????????????"a"????:?'leobert:B:1.0.0'
????????????????]
????????],
????????"APP":?[
????????????????"A":?[
????????????????????????"p":?project(':A'),
????????????????????????"a":?'leobert:A:1.0.0'
????????????????]
????????]
]
問題基本得到解決了,但是并不優(yōu)雅。
優(yōu)雅,優(yōu)雅,優(yōu)雅
我們不妨再修改一下對依賴樹的描述方式,將節(jié)點(diǎn)信息和樹結(jié)構(gòu)分開,重新改進(jìn):
更人道主義的依賴描述
project.ext.deps?=?[
????????"A"??:?["B"],
????????"app":?["A"]
]
project.ext.modules?=?[
????????"A":?[
????????????????"p":?project(':A'),
????????????????"a":?'leobert:A:1.0.0'
????????],
????????"B":?[
????????????????"p"????:?project(':B'),
????????????????"a"????:?'leobert:B:1.0.0'
????????]
]
project.ext.depRules?=?[
????????"B":?"p",
????????"A":?"a"
]
抽象添加依賴的過程,遞歸處理每一個(gè)節(jié)點(diǎn)的依賴收集,并向宿主module添加,當(dāng)某個(gè)節(jié)點(diǎn)在ext.deps中沒有任何依賴時(shí),歸:
ext.utils?=?[
????????????applyDependency:?{?project,?scope,?e?->
????????????????def?rule?=?depRules.get(e)
????????????????def?eInfo?=?ext.modules.get(e)
????????????????println("find?deps?of?"?+?project?+?":rule?is?"?+?rule?+?"?,dep?is:"?+?eInfo.get(rule).toString())
????????????????project.dependencies.add(scope,?eInfo.get(rule))
????????????????def?sub?=?deps.get(e)?//list?deps?of?e
????????????????println("try?to?add?sub?deps?of:"?+?e?+?"?--->?"?+?sub)
????????????????if?(sub?!=?null?&&?!sub.isEmpty())?{
????????????????????sub.each?{?dOfE?->
????????????????????????ext.utils.applyDependency(project,?scope,?dOfE)
????????????????????}
????????????????}
????????????}
????]
每個(gè)module只需要指定自己的scope:
//:app
project.afterEvaluate?{?p?->
????println("handle?deps?for:"?+?p)
????deps.get(p.name).each?{?e?->
????????rootProject.ext.utils.applyDependency(p,"implementation",e)
????}
}
//:A
project.afterEvaluate?{?p?->
????println("handle?deps?for:"?+?p.name)
????deps.get(p.name).each?{?e?->
????????rootProject.ext.utils.applyDependency(p,"compileOnly",e)
????}
}
只要不是獨(dú)立運(yùn)行的module,就是compileOnly,否則就是?implementation。輸出也容易拍錯(cuò):
>?Configure?project?:A
handle?deps?for:A
find?deps?of?project?':A':rule?is?p?,dep?is:project?':B'
try?to?add?sub?deps?of:B?--->?null
>?Configure?project?:app
handle?deps?for:project?':app'
find?deps?of?project?':app':rule?is?a?,dep?is:leobert:A:1.0.0
try?to?add?sub?deps?of:A?--->?[B]
find?deps?of?project?':app':rule?is?p?,dep?is:project?':B'
try?to?add?sub?deps?of:B?--->?null
測試一個(gè)復(fù)雜場景 我們再上圖的基礎(chǔ)上,讓B和Foo依賴Base
project.ext.deps?=?[
????????"app":?["A",?"Foo"],
????????"A"??:?["B",?"Bar"],
????????"Foo":?["Base"],
????????"B"??:?["Base"],
]
project.ext.modules?=?[
????????"A":?[
????????????????"p":?project(':A'),
????????????????"a":?'leobert:A:1.0.0'
????????],
????????"B":?[
????????????????"p":?project(':B'),
????????????????"a":?'leobert:B:1.0.0'
????????],
????????"Foo":?[
????????????????"p":?project(':Foo'),
????????],
????????"Bar":?[
????????????????"p":?project(':Bar'),
????????],
????????"Base":?[
????????????????"p":?project(':Base'),
????????]
]
project.ext.depRules?=?[
????????"B"???:?"p",
????????"A"???:?"a",
????????"Foo"?:?"p",
????????"Bar"?:?"p",
????????"Base":?"p"
]
>?Configure?project?:A
handle?deps?for:A
find?deps?of?project?':A':rule?is?p?,dep?is:project?':B'
try?to?add?sub?deps?of:B?--->?[Base]
find?deps?of?project?':A':rule?is?p?,dep?is:project?':Base'
try?to?add?sub?deps?of:Base?--->?null
find?deps?of?project?':A':rule?is?p?,dep?is:project?':Bar'
try?to?add?sub?deps?of:Bar?--->?null
>?Configure?project?:app
handle?deps?for:project?':app'
find?deps?of?project?':app':rule?is?a?,dep?is:leobert:A:1.0.0
try?to?add?sub?deps?of:A?--->?[B,?Bar]
find?deps?of?project?':app':rule?is?p?,dep?is:project?':B'
try?to?add?sub?deps?of:B?--->?[Base]
find?deps?of?project?':app':rule?is?p?,dep?is:project?':Base'
try?to?add?sub?deps?of:Base?--->?null
find?deps?of?project?':app':rule?is?p?,dep?is:project?':Bar'
try?to?add?sub?deps?of:Bar?--->?null
find?deps?of?project?':app':rule?is?p?,dep?is:project?':Foo'
try?to?add?sub?deps?of:Foo?--->?[Base]
find?deps?of?project?':app':rule?is?p?,dep?is:project?':Base'
try?to?add?sub?deps?of:Base?--->?null
>?Configure?project?:Bar
handle?deps?for:Bar
>?Configure?project?:Base
handle?deps?for:Base
>?Configure?project?:Foo
handle?deps?for:Foo
find?deps?of?project?':Foo':rule?is?p?,dep?is:project?':Base'
try?to?add?sub?deps?of:Base?--->?null
隨著,樹規(guī)模的增大,閱讀依賴關(guān)系還算明顯,但是閱讀日志,又不太優(yōu)雅了。
總結(jié)和展望
我們通過探尋,發(fā)現(xiàn)了一種可以 魚和熊掌兼得 地依賴處理方式,讓我們在Android領(lǐng)域組件化場景下(單項(xiàng)目,多module),能夠靈活地切換:
靜態(tài)包依賴,縮短編譯時(shí)間 項(xiàng)目依賴,快速部署變更進(jìn)行集成測試
對了,上面我們沒有重點(diǎn)提到如何切換,其實(shí)非常地簡單:
只需要修改?project.ext.depRules?中對應(yīng)的配置項(xiàng)即可。
如果后面還有閑情逸致的話,可以再寫一個(gè)studio的插件,獲取?dependency.gradle?的信息, 輸出可視化的依賴樹;rule配置,直接做成多個(gè)開關(guān),優(yōu)雅,永不過時(shí)。

? 耗時(shí)2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!
? 『BATcoder』做了多年安卓還沒編譯過源碼?一個(gè)視頻帶你玩轉(zhuǎn)!
? 重生!進(jìn)階三部曲第一部《Android進(jìn)階之光》第2版 出版!
?BATcoder技術(shù)群,讓一部分人先進(jìn)大廠
大家好,我是劉望舒,騰訊TVP,著有三本業(yè)內(nèi)知名暢銷書,連續(xù)四年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,百度百科收錄的資深技術(shù)專家。
想要加入?BATcoder技術(shù)群,公號(hào)回復(fù)
BAT?即可。
為了防止失聯(lián),歡迎關(guān)注我的小號(hào)
??微信改了推送機(jī)制,真愛請星標(biāo)本公號(hào)??
