共计 5649 个字符,预计需要花费 15 分钟才能阅读完成。
本文作者:郑超
背景
最近团队降级动态代码检测能力,依赖的相干编译检测能力须要用到较新的 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 文件如下:
- R_lib1 = R_lib1;
- R_lib2 = R_lib2;
- R_lib3 = R_lib3;
- R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1 自身的 R)
- R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2 自身的 R)
- 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 文件内联这件事。
测试工程做了一些便于剖析的配置,配置如下:
-
开启 proguard
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. 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>;}
总结
- 从 agp 对 R 文件的解决历史来看,android 编译团队始终在对 R 文件的生成过程一直做优化,并在 agp 4.1.0 版本中彻底解决了 R 文件冗余的问题。
-
编译相干问题剖析思路:
- 先从 app 的构建产物外面剖析相应的后果;
- 波及到有依赖关系剖析的能够将所有 task 的输入输出全副打印进去;
- 1、2 满足不了时,会思考去看相应的源码;
- 最初的大招就是调试编译过程;
- 从云音乐 app 这次 agp 降级的成果来看,app 的体积升高了靠近 7M,编译速度也有很大的晋升,特地是 release 速度快了 10 分钟 +(task 合并),整体收益还是比拟可观的。
文章中应用的测试工程;
参考资料
- Shrink, obfuscate, and optimize your app
- r8
- Android Gradle plugin release notes
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!