本文做者:鄭超前端
最近團隊升級靜態代碼檢測能力,依賴的相關編譯檢測能力須要用到較新的agp,並且目前雲音樂agp版本用的是 3.5.0,對比如今 4.2.0 有較大差距,因此咱們集中對 agp 進行了一次升級。在升級前經過官方文檔,發如今 agp3.6.0 和 4.1.0 版本分別對 R 文件的處理方式進行了相應的升級,具體升級以下。java
Simplified R class generationandroid
The Android Gradle plugin simplifies the compile classpath by generating only one R class for each library module in your project and sharing those R classes with other module dependencies. This optimization should result in faster builds, but it requires that you keep the following in mind:git
從字面意思理解 agp3.6.0
簡化了 R 的生成過程,每個 module 直接生成 R.class
(在 3.6.0 以前 R.class 生成的過程是爲每一個 module 先生成 R.java -> 再經過 javac 生成 R.class ,如今是省去了生成 R.java 和經過 javac 生成 R.class)程序員
如今咱們來驗證一下這個結果,建一個工程,工程中會創建 android library module。分別用 agp3.5.0 和 agp3.6.0 編譯,而後看構建產物。github
從構建產物上來看也驗證了這個結論,agp 3.5.0 到 3.6.0 經過減小 R 生成的中間過程,來提高 R 的生成效率(先生成 R.java 再經過 javac 生成 R.class 變爲直接生成 R.class);api
App size significantly reduced for apps using code shrinkingmarkdown
Starting with this release, fields from R classes are no longer kept by default, which may result in significant APK size savings for apps that enable code shrinking. This should not result in a behavior change unless you are accessing R classes by reflection, in which case it is necessary to add keep rules for those R classes.架構
從標題看 apk 包體積有顯著減小
(這個太有吸引力了),經過下面的描述,大體意思是再也不保留 R 的 keep 規則,也就是 app 中再也不包括 R 文件?(要不怎麼減小包體積的)app
在分析這個結果以前先介紹下 apk 中,R 文件冗餘的問題;
android 從 ADT 14 開始爲了解決多個 library 中 R 文件中 id 衝突,因此將 Library 中的 R 的改爲 static 的很是量屬性。
在 apk 打包的過程當中,module 中的 R 文件採用對依賴庫的R進行累計疊加的方式生成。若是咱們的 app 架構以下:
編譯打包時每一個模塊生成的 R 文件以下:
在最終打成 apk 時,除了 R_app(由於 app 中的 R 是常量,在 javac 階段 R 引用就會被替換成常量,因此打 release 混淆時,app 中的 R 文件會被 shrink 掉),其他的 R 文件所有都會打進 apk 包中。這就是 apk 中 R 文件冗餘的由來。並且若是項目依賴層次越多,上層的業務組件越多,將會致使 apk 中的 R 文件將急劇的膨脹。
系統致使的冗餘問題,總不會難住聰明的程序員。在業內目前已經有一些R文件內聯的解決方案。大體思路以下:
因爲 R_app 是包括了全部依賴的的 R,因此能夠自定義一個 transform 將全部 library module 中 R 引用都改爲對 R_app 中的屬性引用,而後刪除全部依賴庫中的 R 文件。這樣在 app 中就只有一個頂層 R 文件。(這種作法不是很是完全,在 apk 中仍然保留了一個頂層的 R,更完全的能夠將全部代碼中對 R 的引用都替換成常量,並在 apk 中刪除頂層的 R )
首先咱們分別用 agp 4.1.0 和 agp 3.6.0 構建 apk 進行一個對比,從最終的產物來確認下是否作了 R 文件內聯這件事。 測試工程作了一些便於分析的配置,配置以下:
buildTypes {
release {
minifyEnabled true // 打開
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
複製代碼
// proguard-rules.pro中配置
-dontobfuscate
複製代碼
構建 release 包。 先看下 agp 3.6.0 生成的 apk:
從圖中能夠看到 bizlib
module 中會有 R 文件,查看 SecondActivity
的 byte code ,會發現內部有對 R 文件的引用。
接着再來看 agp 4.1.0 生成的 apk:
能夠看到,bizlib
module 中已經沒有 R 文件,而且查看 SecondActivity
的 byte code ,會發現內部的引用已經變成了一個常量。
由此能夠肯定,agp 4.1.0 是作了對 R 文件的內聯,而且作的很完全,不只刪除了冗餘的 R 文件,而且還把全部對 R 文件的引用都改爲了常量。
如今咱們來具體分析下 agp 4.1.0 是如何作到 R 內聯的,首先咱們大體分析下,要對 R 作內聯,基本能夠猜測到是在 class 到 dex 這個過程當中作的。肯定了大體階段,那接下看能不能從構建產物來縮小相應的範圍,最好能精確到具體的 task。(題外話:分析編譯相關問題通常四板斧:1. 先從 app 的構建產物裏面分析相應的結果;2.涉及到有依賴關係分析的能夠將全部 task 的輸入輸出所有打印出來;3. 一、2知足不了時,會考慮去看相應的源碼;4. 最後的大招就是調試編譯過程;)
首先咱們看下構建產物裏面的 dex,以下圖:
接下來在 app module 中增長全部 task 輸入輸出打印的 gradle 腳原本輔助分析,相關腳本以下:
gradle.taskGraph.afterTask { task ->
try {
println("---- task name:" + task.name)
println("-------- inputs:")
task.inputs.files.each { it ->
println(it.absolutePath)
}
println("-------- outputs:")
task.outputs.files.each { it ->
println(it.absolutePath)
}
} catch (Exception e) {
}
}
複製代碼
minifyReleaseWithR8
相應的輸入輸出以下:
從圖中能夠看出,輸入有整個 app 的 R 文件的集合(R.jar),因此基本明確 R 的內聯就是在 minifyReleaseWithR8
task 中處理的。
接下來咱們就具體分析下這個 task。 具體的邏輯在 R8Task.kt
裏面.
建立 minifyReleaseWithR8
task 代碼以下:
class CreationAction(
creationConfig: BaseCreationConfig,
isTestApplication: Boolean = false
) : ProguardConfigurableTask.CreationAction<R8Task, BaseCreationConfig>(creationConfig, isTestApplication) {
override val type = R8Task::class.java
// 建立 minifyReleaseWithR8 task
override val name = computeTaskName("minify", "WithR8")
.....
}
複製代碼
task 執行過程以下(因爲代碼過多,下面僅貼出部分關鍵節點):
// 1. 第一步,task 具體執行
override fun doTaskAction() {
......
// 執行 shrink 操做
shrink(
bootClasspath = bootClasspath.toList(),
minSdkVersion = minSdkVersion.get(),
......
)
}
// 2. 第二步,調用 shrink 方法,主要作一些輸入參數和配置項目的準備
companion object {
fun shrink( bootClasspath: List<File>, ...... ) {
......
// 調用 r8Tool.kt 中的頂層方法,runR8
runR8(
filterMissingFiles(classes, logger),
output.toPath(),
......
)
}
// 3. 第三步,調用 R8 工具類,執行混淆、優化、脫糖、class to dex 等一系列操做
fun runR8( inputClasses: Collection<Path>, ...... ) {
......
ClassFileProviderFactory(libraries).use { libraryClasses ->
ClassFileProviderFactory(classpath).use { classpathClasses ->
r8CommandBuilder.addLibraryResourceProvider(libraryClasses.orderedProvider)
r8CommandBuilder.addClasspathResourceProvider(classpathClasses.orderedProvider)
// 調用 R8 工具類中的run方法
R8.run(r8CommandBuilder.build())
}
}
}
複製代碼
至此能夠知道實際上 agp 4.1.0 中是經過 R8 來作到 R 文件的內聯的。那 R8 是若是作到的呢?這裏簡要描述下,再也不作具體代碼的分析:
R8 從能力上是包括了 Proguard 和 D8(java脫糖、dx、multidex),也就是從 class 到 dex 的過程,並在這個過程當中作了脫糖、Proguard 及 multidex 等事情。在 R8 對代碼作 shrink 和 optimize 時會將代碼中對常量的引用替換成常量值。這樣代碼中將不會有對 R 文件的引用,這樣在 shrink 時就會將 R 文件刪除。
固然要達到這個效果 agp 在 4.1.0 版本里面對默認的 keep 規則也要作一些調整,4.1.0 裏面刪除了默認對 R 的 keep 規則,相應的規則以下:
-keepclassmembers class **.R$* {
public static <fields>;
}
複製代碼
從 agp 對 R 文件的處理歷史來看,android 編譯團隊一直在對R文件的生成過程不斷作優化,並在 agp 4.1.0 版本中完全解決了 R 文件冗餘的問題。
編譯相關問題分析思路:
從雲音樂 app 此次 agp 升級的效果來看,app 的體積下降了接近 7M,編譯速度也有很大的提高,特別是 release 速度快了 10 分鐘+(task 合併),總體收益仍是比較可觀的。
文章中使用的測試工程;
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!