Android全量編譯加速——(透明依賴)
1.1. 背景
在我們平常的開發(fā)中構(gòu)建工程是一個(gè)基礎(chǔ)的環(huán)節(jié),決定著開發(fā)效率的高低,然而隨著業(yè)務(wù)代碼不斷累積,編譯耗時(shí)也隨之增長(zhǎng)。雖然已經(jīng)有許多增量編譯加速方案,但不可避免的在很多場(chǎng)景,還是需要全量編譯。而對(duì)于全量編譯加速,我們遇到了一些困難:
- K歌的項(xiàng)目里,總代碼量160w行,kotlin代碼占比43%左右,編譯耗時(shí)占比卻高達(dá)70%,必須要壓縮這個(gè)比例。
于是需要找到一種方法,既能繼續(xù)享受kotlin帶來的開發(fā)便利,也能緩解全量編譯時(shí)間快速增長(zhǎng)的問題。
1.2. 方案
如果能減少kotlin的編譯數(shù)量,就能降低編譯耗時(shí),要么減少代碼,要么提前編譯代碼,后者可行度高。
而Android里支持兩種二進(jìn)制歸檔文件:JAR、AAR
兩種格式里源碼都是以.class格式存在,不過jar不包含資源,對(duì)于在做組件化的項(xiàng)目不友好,library module在編譯后會(huì)直接生成aar。
那么只要把所有l(wèi)ibrary module都通過CI/CD工具,持續(xù)的自動(dòng)生成aar,發(fā)布到同一個(gè)maven倉庫,在編譯時(shí)用這些aar參與編譯就成功了。
1.3. 方案初步呈現(xiàn)
library modules提前編譯成了aar,我們需要把依賴類型由implementation project更改為implementation aar。
如果library module代碼改變了,都需要重復(fù)執(zhí)行提前編譯aar,修改依賴版本號(hào),很浪費(fèi)時(shí)間,這里能不能取締這個(gè)重復(fù)操作環(huán)節(jié)讓程序自動(dòng)化?
一種更好的方式:編譯時(shí)判斷l(xiāng)ibrary module當(dāng)前代碼版本是否有可用的aar,有則使用aar參與編譯。拆解流程:
計(jì)算當(dāng)前代碼版本所有文件的hash,包含如下:
- JavaSource
- JavaResource
- Assets
- Resources
- Aidl
- JniLibs
- AndroidManifest.xml
- Proguard
- Lint
判斷maven倉庫里是否有對(duì)應(yīng)hash的aar,尋址 = repository/libraryName/version-md5
修改library module依賴類型為aar。
1.4. 遇到的問題
1. jar重復(fù)類沖突
可以看到B對(duì)C存在直接的依賴關(guān)系,這個(gè)關(guān)系會(huì)聲明在B.arr的元數(shù)據(jù)文件.pom,又由于C的代碼更改了導(dǎo)致無法匹配遠(yuǎn)程aar,所以最后C會(huì)同時(shí)以aar和project兩種方式參與編譯,如果C里包含了jar,就會(huì)沖突。
2. 工程重復(fù)類沖突
share_m和share是同一個(gè)代碼倉庫,開發(fā)便于驗(yàn)證更改了name,路由不一樣代碼一樣,gradle認(rèn)為是兩個(gè)aar,報(bào)錯(cuò)重復(fù)。
3. 三方庫版本沖突
最終編譯后share代碼版本依然為1.2.0,因?yàn)锽.aar存在對(duì)share:1.2.0依賴。Gradle將考慮所有請(qǐng)求的版本,無論它們出現(xiàn)在依賴關(guān)系圖中的何處。在這些版本中,它將選擇最高的版本。
第一個(gè)問題:明顯的需要把B(aar)—>C(aar)這個(gè)依賴項(xiàng)解除,這里常用有兩個(gè)辦法:
- 直接從pom里刪除該項(xiàng)依賴元數(shù)據(jù)(K歌采用)。
- 修改B依賴C的依賴類型改為compileOnly,不過如果B使用了C的資源打包aar會(huì)報(bào)錯(cuò)。
第二個(gè)問題:K歌的做法是要求name保持一致,開發(fā)修改代碼發(fā)布時(shí)只改變version。
第三個(gè)問題:因?yàn)檫@種模型也會(huì)存在正常開發(fā)中,對(duì)于版本沖突,有以下幾項(xiàng)辦法:
- 開發(fā)時(shí)用更高的版本去覆蓋掉參與構(gòu)建的所有版本。
- 修改B—>share:1.2.0依賴類型為compileOnly,來解除傳遞依賴。
- 如果一定要使用動(dòng)態(tài)版本號(hào)+,且低于參與構(gòu)建的版本,可以提取出白名單,從pom里刪除該項(xiàng)依賴,統(tǒng)一由app主module依賴(K歌采用)。
- B在發(fā)布aar時(shí),不保留pom里對(duì)三方的任何依賴元數(shù)據(jù),編譯時(shí)統(tǒng)一由app依賴。
想要解決傳遞依賴的問題還有常見的transitive,force,嚴(yán)格依賴等特性,K歌使用這些特性很少,考慮到要開發(fā)透明,保持原有代碼,我們采用的都是直接修改pom文件依賴項(xiàng)來解除傳遞依賴。
從以上問題不難看出,唯一標(biāo)識(shí)=自身內(nèi)容+依賴關(guān)系圖,所以在計(jì)算md5時(shí),我們也需要把依賴關(guān)系算進(jìn)去。什么時(shí)候可以獲取依賴圖?
Gradle的構(gòu)建生命周期分為3步:
1、初始化
Gradle支持單項(xiàng)目和多項(xiàng)目構(gòu)建。在初始化階段,Gradle確定將要參與構(gòu)建的項(xiàng)目,并為每個(gè)項(xiàng)目創(chuàng)建一個(gè)Project實(shí)例。
2、配置
在此階段,將配置項(xiàng)目對(duì)象。執(zhí)行作為構(gòu)建一部分的所有項(xiàng)目的構(gòu)建腳本。
3、執(zhí)行
Gradle確定要在配置階段創(chuàng)建和配置的任務(wù)子集。子集由傳遞給gradle命令的任務(wù)名稱參數(shù)和當(dāng)前目錄確定。然后Gradle執(zhí)行每個(gè)選定的任務(wù)。
明確在配置階段是執(zhí)行build.gradle,依賴圖生成后,可以在項(xiàng)目評(píng)估回調(diào)里(afterEvaluate)解析完成我們的操作。
K歌的app module依賴了全局所有的library module,在編譯時(shí)app最先收到評(píng)估回調(diào),只要這時(shí)修改app的依賴關(guān)系圖就能阻斷其余l(xiāng)ibrary module的后續(xù)配置流程,而這時(shí)library module并未評(píng)估完成,拿不到依賴關(guān)系圖就無法計(jì)算md5,只能手動(dòng)解析library module的build.gradle文件里的依賴配置。K歌的配置一部分直接申明在dependencies里,一部分提取成了統(tǒng)一管理versionConfigs.gradle。
需要注意的是,每個(gè)項(xiàng)目評(píng)估完整結(jié)束后再修改依賴圖是不安全的,Gradle會(huì)阻止。
1.5. 最終流程
- 構(gòu)建項(xiàng)目,處于配置階段時(shí)會(huì)執(zhí)行每個(gè)project的build.gradle,里面會(huì)確定下來依賴關(guān)系,在評(píng)估項(xiàng)目之后(afterEvaluate)收到通知。
- 解析配置里對(duì)于本地project類型的依賴(DefaultProjectDependency),計(jì)算project的md5,計(jì)算包含的內(nèi)容為前面講訴的aar內(nèi)容,同時(shí)把project的依賴關(guān)系也要作為md5計(jì)算的范圍。
- 計(jì)算出md5后按照maven庫的尋址規(guī)則拼接到路徑上訪問遠(yuǎn)端maven倉庫是否存在此aar。
- 存在aar,則將本地project的依賴類型改為遠(yuǎn)程aar依賴(DefaultExternalModuleDependency)。