Android agp 對 R 文件內聯支持

本文做者:鄭超前端

背景

最近團隊升級靜態代碼檢測能力,依賴的相關編譯檢測能力須要用到較新的agp,並且目前雲音樂agp版本用的是 3.5.0,對比如今 4.2.0 有較大差距,因此咱們集中對 agp 進行了一次升級。在升級前經過官方文檔,發如今 agp3.6.0 和 4.1.0 版本分別對 R 文件的處理方式進行了相應的升級,具體升級以下。java

agp 3.6.0 變動

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

  • Because the compiler shares R classes with upstream module dependencies, it’s important that each module in your project uses a unique package name.
  • The visibility of a library's R class to other project dependencies is determined by the configuration used to include the library as a dependency. For example, if Library A includes Library B as an 'api' dependency, Library A and other libraries that depend on Library A have access to Library B's R class. However, other libraries might not have access to Library B's R class If Library A uses the implementation dependency configuration. To learn more, read about dependency configurations.

從字面意思理解 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 構建產物以下:

image

agp 3.6.0 構建產物以下:

image

從構建產物上來看也驗證了這個結論,agp 3.5.0 到 3.6.0 經過減小 R 生成的中間過程,來提高 R 的生成效率(先生成 R.java 再經過 javac 生成 R.class 變爲直接生成 R.class);api

agp 4.1.0升級以下:

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 文件冗餘的問題;

R 文件冗餘問題

android 從 ADT 14 開始爲了解決多個 library 中 R 文件中 id 衝突,因此將 Library 中的 R 的改爲 static 的很是量屬性。

在 apk 打包的過程當中,module 中的 R 文件採用對依賴庫的R進行累計疊加的方式生成。若是咱們的 app 架構以下:

image

編譯打包時每一個模塊生成的 R 文件以下:

  1. R_lib1 = R_lib1;
  2. R_lib2 = R_lib2;
  3. R_lib3 = R_lib3;
  4. R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1自己的R)
  5. R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2自己的R)
  6. R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app自己R)

在最終打成 apk 時,除了 R_app(由於 app 中的 R 是常量,在 javac 階段 R 引用就會被替換成常量,因此打 release 混淆時,app 中的 R 文件會被 shrink 掉),其他的 R 文件所有都會打進 apk 包中。這就是 apk 中 R 文件冗餘的由來。並且若是項目依賴層次越多,上層的業務組件越多,將會致使 apk 中的 R 文件將急劇的膨脹。

R 文件內聯(解決冗餘問題)

系統致使的冗餘問題,總不會難住聰明的程序員。在業內目前已經有一些R文件內聯的解決方案。大體思路以下:

因爲 R_app 是包括了全部依賴的的 R,因此能夠自定義一個 transform 將全部 library module 中 R 引用都改爲對 R_app 中的屬性引用,而後刪除全部依賴庫中的 R 文件。這樣在 app 中就只有一個頂層 R 文件。(這種作法不是很是完全,在 apk 中仍然保留了一個頂層的 R,更完全的能夠將全部代碼中對 R 的引用都替換成常量,並在 apk 中刪除頂層的 R )

agp 4.1.0 R 文件內聯

首先咱們分別用 agp 4.1.0 和 agp 3.6.0 構建 apk 進行一個對比,從最終的產物來確認下是否作了 R 文件內聯這件事。 測試工程作了一些便於分析的配置,配置以下:

  1. 開啓 proguard
buildTypes {
    release {
        minifyEnabled true // 打開
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
複製代碼
  1. 關閉混淆,僅保留壓縮和優化(避免混淆打開,帶來的識別問題)
// proguard-rules.pro中配置
-dontobfuscate
複製代碼

構建 release 包。 先看下 agp 3.6.0 生成的 apk:

image

從圖中能夠看到 bizlib module 中會有 R 文件,查看 SecondActivity 的 byte code ,會發現內部有對 R 文件的引用。

接着再來看 agp 4.1.0 生成的 apk:

image

能夠看到,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,以下圖:

image

接下來在 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 相應的輸入輸出以下:

image

從圖中能夠看出,輸入有整個 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>;
}
複製代碼

總結

  1. 從 agp 對 R 文件的處理歷史來看,android 編譯團隊一直在對R文件的生成過程不斷作優化,並在 agp 4.1.0 版本中完全解決了 R 文件冗餘的問題。

  2. 編譯相關問題分析思路:

    1. 先從 app 的構建產物裏面分析相應的結果;
    2. 涉及到有依賴關係分析的能夠將全部 task 的輸入輸出所有打印出來;
    3. 一、2知足不了時,會考慮去看相應的源碼;
    4. 最後的大招就是調試編譯過程;
  3. 從雲音樂 app 此次 agp 升級的效果來看,app 的體積下降了接近 7M,編譯速度也有很大的提高,特別是 release 速度快了 10 分鐘+(task 合併),總體收益仍是比較可觀的。

文章中使用的測試工程

參考資料

  1. Shrink, obfuscate, and optimize your app
  2. r8
  3. Android Gradle plugin release notes

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!

相關文章
相關標籤/搜索