乐趣区

关于android:Android-agp-对-R-文件内联支持

本文作者:郑超

背景

最近团队降级动态代码检测能力,依赖的相干编译检测能力须要用到较新的 agp,而且目前云音乐 agp 版本用的是 3.5.0,比照当初 4.2.0 有较大差距,所以咱们集中对 agp 进行了一次降级。在降级前通过官网文档,发现在 agp3.6.0 和 4.1.0 版本别离对 R 文件的解决形式进行了相应的降级, 具体降级如下。

agp 3.6.0 变更

Simplified R class generation

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:

  • 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 编译,而后看构建产物。

agp 3.5.0 构建产物如下:

agp 3.6.0 构建产物如下:

从构建产物上来看也验证了这个论断,agp 3.5.0 到 3.6.0 通过缩小 R 生成的两头过程,来晋升 R 的生成效率(学生成 R.java 再通过 javac 生成 R.class 变为间接生成 R.class);

agp 4.1.0 降级如下:

App size significantly reduced for apps using code shrinking

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 文件?(要不怎么缩小包体积的)

在剖析这个后果之前先介绍下 apk 中,R 文件冗余的问题;

R 文件冗余问题

android 从 ADT 14 开始为了解决多个 library 中 R 文件中 id 抵触,所以将 Library 中的 R 的改成 static 的十分量属性。

在 apk 打包的过程中,module 中的 R 文件采纳对依赖库的 R 进行累计叠加的形式生成。如果咱们的 app 架构如下:

编译打包时每个模块生成的 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'
     }
    }
  2. 敞开混同,仅保留压缩和优化(防止混同关上,带来的辨认问题)

    // 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. 1、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>;}

总结

  1. 从 agp 对 R 文件的解决历史来看,android 编译团队始终在对 R 文件的生成过程一直做优化,并在 agp 4.1.0 版本中彻底解决了 R 文件冗余的问题。
  2. 编译相干问题剖析思路:

    1. 先从 app 的构建产物外面剖析相应的后果;
    2. 波及到有依赖关系剖析的能够将所有 task 的输入输出全副打印进去;
    3. 1、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!

退出移动版