本文做者:郑超前端
最近团队升级静态代码检测能力,依赖的相关编译检测能力须要用到较新的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!