以下文章来源于程序员江同学 ,作者程序员江同学
专注于 Android 及 Kotlin 知识分享
关于 Android 编译加速的文章相信大家都看过不少,但常常要么是好几年前写的,目前看来有些过时;要么介绍了一大堆配置,最后一实践发现并没有多大效果;要么就是大厂黑科技,但是没有开源。
今天我们就一起来看看,在2022年AGP7.0时代,除了传统的开启build-cache,打开并行编译,调整Gradle堆内存大小等常用手段之外,还有哪些可以落地的编译加速实用技巧。
既然是2022年编译加速的实用技巧,首先就是要求编译工具链的版本比较新,后面介绍的技巧大部分也是新引入的特性。
几乎每次更新时,Android 编译工具链都会得到一定性能上的优化或者是引入新的功能,因此我们应该及时跟进Gradle,Android Gradle Plugin和Kotlin Gradle Plugin等工具的更新,才能及时获得到相应的性能提升。
Transform API是AGP1.5就引入的特性,主要用于在Android构建过程中,在Class转Dex的过程中修改Class字节码。利用Transform API,我们可以拿到所有参与构建的Class文件,然后可以借助ASM 等字节码编辑工具进行修改,插入自定义逻辑。
国内很多团队都或多或少的用AGP的Transform API来搞点儿黑科技,比如无痕埋点,耗时统计,方法替换等。但是在AGP7.0中Transform已经被标记为废弃了,并且将在AGP8.0中移除。
在AGP7.0之后,可以使用AsmClassVisitorFactory来做插桩,根据官方的说法,AsmClassVisitoFactory会带来约18%的性能提升,同时可以减少约5倍代码
AsmClassVisitorFactory之所以比Transform在性能上有优势,主要在于节省了IO的时间。
如上图所示,多个Transform相互独立,都需要通过IO读取输入,修改字节码后将结果通过IO输出,供下一个Transform使用,如果每个Transform操作IO耗时+10s的话,各个Transform叠在一起,编译耗时就会呈线性增长
而使用AsmClassVisitorFactory则不需要我们手动进行IO操作,这是因为AsmInstrumentationManager中已经做了统一处理,只需要进行一次IO操作,然后交给ClassVisitor列表处理,完成后统一输出。
通过这种方式,可以有效地减少IO操作,减少耗时。其实国内之前滴滴开源的Booster与字节开源的Bytex,都是通过这种思路来优化Transform性能的,现在官方终于跟进了
总得来说,AsmClassVisitorFactory在性能上与易用性上都有一定的提升,具体用法可参见:Transform 被废弃,ASM 如何适配?
注解处理器是Android开发中一种常用的技术,很多常用的框架比如ButterKnife,ARouter,Glide中都使用到了注解处理器相关技术。
但是如果项目比较大的话,会很容易发现KAPT是拖慢编译速度的常见原因,这也是谷歌推出KSP取代KAPT的原因
从上面这张图其实就可以看出KAPT慢的原因了,KAPT 通过与 Java 注解处理基础架构相结合,让大部分Java语言注解处理器能够在Kotlin中开箱即用。
为此,KAPT 首先需要将 Kotlin 代码编译成 JavaStubs,这些JavaStubs中保留了Java注释处理器关注的信息。
这意味着编译器必须多次解析程序中的所有符号 (一次生成JavaStubs,另一次完成实际编译),但是生成JavaStubs的过程是非常耗时的,往往生成Java Stubs的时间比APT真正处理注解的时间要长。
而KSP不需要生成JavaStubs,而是作为Kotlin编译器插件运行。它可以直接处理Kotlin符号,而不需要依赖Java注解处理基础架构。
因为KSP相比KAPT少了生成JavaStubs的过程,因此通常可以得到100%以上的速度提升。关于KSP的具体使用方法可参见:使用 Kotlin Symbol Processing 1.0 缩短 Kotlin 构建时间。
目前KSP已经发布了稳定版了,像Room,Moshi等库也已经做了适配,对于这些已经适配了的库,我们可以直接迁移。
但还是有一些常用的库比如Glide,ARouter还没有做适配,这些库是我们移除KAPT最大的障碍。
下面给出一些还不支持KSP的库的过渡迁移方法
更新:Glide最新版本已经支持了KSP,可以直接升级接入了。
我们知道,Gradle 的生命周期可以分为大的三个部分:初始化阶段(Initialization Phase),配置阶段(Configuration Phase),执行阶段(Execution Phase),如下图所示:
在任务执行阶段,Gradle提供了多种方式实现Task的缓存与重用(如up-to-date检测,增量编译,build-cache等)。
除了任务执行阶段,任务配置阶段有时也比较耗时,目前AGP也支持了配置阶段缓存Configuration Cache,它可以缓存配置阶段的结果,当脚本没有发生改变时可以重用之前的结果。
在越大的项目中配置阶段缓存的收益越大,module比较多的项目可能每次执行都要先配置20到30秒,尤其是增量编译时,配置的耗时可能都跟执行的耗时差不多了,而这正是configuration-cache的用武之地。
目前Configuration-cache还是实验特性,如果你想要开启的话可以在gradle.properties中添加以下代码:
# configuration cache
org.gradle.unsafe.configuration-cache=true
org.gradle.unsafe.configuration-cache-problems=warn
开启了Configuration cache之后效果还是比较明显的,如果构建脚本没有发生变化可以直接跳过配置阶段
Android官方给出了一个开启Configuration cache前后的对比,可以看出在这个benchmark中可以得到约30%的提升(当然是在配置阶段耗时占比越高的时候效果越明显,全量编译时应该不会有这样的比例)。
当然打开Configuration Cache之后可能会有一些适配问题,如果是第三方插件,发现常用插件出现不支持的情况,可先搜索是否有相同的问题已经出现并修复。
如果是项目中自定义Task不支持的话,还需要适配一下Configuration Cache,适配Configuration Cache的核心思路其实很简单:不要在Task执行阶段调用外部不可序列化的对象(比如Project与Variant)。
不过如果你的项目中自定义Task比较多的话,适配Configuration Cache可能是个体力活,比如 AGP 兼容 Configuration Cache 就修了 400 多个 ISSUE。
如需详细了解配置缓存,请参阅配置缓存深度解析和有关配置缓存的 Gradle 文档。
Jetifier是android support包迁移到androidX的工具,当你在项目中启动用Jetifier时 ,Gradle插件会在构建时将三方库里的Support转换成AndroidX,因此会对构建速度产生影响。
同时Jetfier也会对sync耗时产生比较大的影响,详情可见B站大佬的分析:
Jetifier在AndroidX刚出现时是一个非常实用的工具,可以帮助我们快速的迁移到AndroidX。但是到了2022年,相信绝大多数库都已经迁移到了AndroidX,Jetifier的历史使命可以说已经完成了,因此是时候移除Jetifier了
AGP7.0已经提供了工具供我们检查每个module能否移除Jetifier,直接运行./gradlew checkJetifier即可,通过以下命令检查所有module的Jetifier使用情况
task checkJetifierAll(group: "verification") { }
subprojects { project ->
project.tasks.whenTaskAdded { task ->
if (task.name == "checkJetifier") {
checkJetifierAll.dependsOn(task)
}
}
}
通过运行./gradlew checkJetifierAll就可以打印出所有module的Jetifier使用情况
在明确了哪些库还不支持Jetifier之后,可以一步步开始迁移了。
// https://developer.android.com/studio/command-line/jetifier
./jetifier-standalone -i <source-library> -o <output-library>
在 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)
可以看出各个模块的R文件都会包含上层组件的R文件内容,这不仅会带来包体积问题,也会给编译速度带来一定的影响。比如我们在R_lib1中添加了资源,所有下游模块的R文件都需要重新编译。
从 Android Studio Bumblebee 开始,新项目的非传递 R 类默认处于开启状态。即gradle.properties文件中都开启了如下标记
android.nonTransitiveRClass=true
对于使用早期版本的 Studio 创建的项目,您可以依次前往 Refactor > Migrate to Non-transitive R Classes,将项目更新为使用非传递 R 类。
使用组件化多模块开发的同学都有经验,当我们修改底层模块(比如util模块)时,所有依赖于这个模块的上层模块都需要重新编译,Kotlin的增量编译在这种情况往往是不生效的,这种时候的编译往往非常耗时。
在Kotlin 1.7.0中,Kotlin编译器对于跨模块增量编译也做了支持,并且与Gradle构建缓存兼容,对编译避免的支持也得到了改进。这些改进减少了模块和文件重新编译的次数,让整体编译更加迅速。
首先来看下Kotlin官方的数据,以下基准测试结果是在Kotlin项目中的kotlin-gradle-plugin模块上测得:
可以看出,当缓存命中时有86%到96%的加速效果,当缓存没有命中时也有26%的加速效果。
我在项目中开启后实测效果也很不错,修改一个底层模块,在特性开启前需要耗时4分钟左右,开启后增量编译耗时减少到30到40s,加速约85%。
在 gradle.properties 文件中设置以下选项即可使用新方式进行增量编译:
kotlin.incremental.useClasspathSnapshot=true // 开启跨模块增量编译
kotlin.build.report.output=file // 可选,启用构建报告
可以看出,开启步骤还是非常简单的,关于Kotlin跨模块增量编译的原理可参见:Kotlin 增量编译的新方式
https://blog.jetbrains.com/zh-hans/kotlin/2022/07/a-new-approach-to-incremental-compilation-in-kotlin/
对于增量编译,稳定性和可靠性至关重要。有时增量编译总会失效,Kotlin 1.7同样支持为编译任务创建编译报告,报告包含不同编译阶段的持续时间以及无法使用增量编译的原因,可以帮助你定位为什么增量编译失效了。
关于编译报告的启用与使用可见:隆重推出 Kotlin 构建报告
https://blog.jetbrains.com/zh-hans/kotlin/2022/07/introducing-kotlin-build-reports/
除了上述的软件方向的一系列优化,也可以从硬件方向进行优化,也就是升级你的电脑配置。
个人感觉影响编译速度的关键基本配置如下:
从硬件方向入手,有时也可以得到不错的优化效果,充钱是真的可以变强的。
本文主要介绍了编译加速的8个实用技巧,有的接入起来非常简单,有的则需要一定的适配成本,但都是可以落地的并且有一定效果的编译加速技巧。
总得来说,为了充分利用最新的优化技巧与各种新功能,我们应该及时跟进android编译工具链的更新。
如果本文对你有所帮助,欢迎点赞收藏~
Android官方文档 - 优化构建速度
How we reduced our Gradle build times by over 80%
10 ideas to improve your Gradle build times
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
点击 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!