作者:程序员江同学
转载地址:https://juejin.cn/post/7153250843905654798
关于 Android
编译加速的文章相信大家都看过不少,但常常要么是好几年前写的,目前看来有些过时;要么介绍了一大堆配置,最后一实践发现并没有多大效果;要么就是大厂黑科技,但是没有开源。
今天我们就一起来看看,在2022年AGP7.0
时代,除了传统的开启build-cache
,打开并行编译,调整Gradle
堆内存大小等常用手段之外,还有哪些可以落地的编译加速实用技巧
既然是2022年编译加速的实用技巧,首先就是要求编译工具链的版本比较新,后面介绍的技巧大部分也是新引入的特性
几乎每次更新时,Android
编译工具链都会得到一定性能上的优化或者是引入新的功能,因此我们应该及时跟进Gradle
,Android Gradle Plugin
和Kotlin Gradle Plugin
等工具的更新,才能及时获得到相应的性能提升
Transform
迁移到AsmClassVisitorFactory
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
的库的过渡迁移方法
KAPT
一般就是用来生成代码,像Glide
这种生成的代码比较稳定的库(通常只有几个@GlideModule
),可以直接把生成的代码从build
目录拷贝到项目中来,然后移除KAPT
,后续如果有新的@GlideModule
再更新下生成的文件(当然这样可能不太方便,只是一种过渡的方式,等待Glide
官方更新)ARouter
这种生成代码不断增加的库(不断有新的@ARouter
注解),上面的方式就不太适用了。考虑到ARoutr
已经很久没有更新了,可以考虑迁移到一个不使用KAPT
的新的路由库Configuration Cache
我们知道,Gradle
的生命周期可以分为大的三个部分:初始化阶段(Initialization Phase
),配置阶段(Configuration Phase
),执行阶段(Execution Phase
),如下图所示:
其中任务执行的部分只要处理恰当,已经能够很好的进行缓存和重用,如果把这个机制运用到其他阶段当然也能带来一些收益。
仅次于执行阶段耗时的一般是配置阶段, AGP
现在也支持了配置阶段缓存 Configuration Cache
,它使得配置阶段的主要产出物:Task Graph
可以被重用
在越大的项目中配置阶段缓存的收益越大,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
适配当然打开Configuration Cache
之后可能会有一些适配问题,如果是第三方插件,发现常用插件出现不支持的情况,可先搜索是否有相同的问题已经出现并修复
如果是项目中自定义Task
不支持的话,还需要适配一下Configuration Cache
,适配Configuration Cache
的核心思路其实很简单:不要在Task
执行阶段调用外部不可序列化的对象(比如Project
与Variant
)
不过如果你的项目中自定义Task
比较多的话,适配Configuration Cache
可能是个体力活,比如 AGP
兼容 Configuration Cache
就修了 400 多个 ISSUE
如需详细了解配置缓存,请参阅配置缓存深度解析和有关配置缓存的 Gradle 文档
Jetifier
Jetifier
是android support
包迁移到androidX
的工具,当你在项目中启动用Jetifier
时 ,Gradle
插件会在构建时将三方库里的Support
转换成AndroidX
,因此会对构建速度产生影响。
同时Jetfier
也会对sync
耗时产生比较大的影响,详情可见B站大佬的分析:哔哩哔哩 Android 同步优化•Jetifier
Jetifier
在AndroidX
刚出现时是一个非常实用的工具,可以帮助我们快速的迁移到AndroidX
。但是到了2022年,相信绝大多数库都已经迁移到了AndroidX
,Jetifier
的历史使命可以说已经完成了,因此是时候移除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
之后,可以一步步开始迁移了
androidX
的最新版本, 如果有直接升级即可android.useAndroidX = true
,迁移到AndroidX
,然后升级所有的依赖,发布个新版本就可以了。AAR/JAR
转成jetified
之后的AAR/JAR
。这个命令行的转换效果和你在代码里开启android.enableJetifier
的效果是一样的。命令行如下:// https://developer.android.com/studio/command-line/jetifier
./jetifier-standalone -i -o
R
文件传递在 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
文件都需要重新编译。
R
文件传递可以通过编译避免的方式获得更快的编译速度R
文件传递有助于确保每个模块的R
类仅包含对其自身资源的引用,避免无意中引用其他模块资源,明确模块边界。R
文件传递也可以减少很大一部分包体积与dex
数量从 Android Studio Bumblebee
开始,新项目的非传递 R
类默认处于开启状态。即gradle.properties
文件中都开启了如下标记
android.nonTransitiveRClass=true
对于使用早期版本的 Studio
创建的项目,您可以依次前往 Refactor > Migrate to Non-transitive R Classes
,将项目更新为使用非传递 R
类。
Kotlin
跨模块增量编译使用组件化多模块开发的同学都有经验,当我们修改底层模块(比如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 增量编译的新方式
对于增量编译,稳定性和可靠性至关重要。有时增量编译总会失效,Kotlin 1.7
同样支持为编译任务创建编译报告,报告包含不同编译阶段的持续时间以及无法使用增量编译的原因,可以帮助你定位为什么增量编译失效了
关于编译报告的启用与使用可见:隆重推出 Kotlin 构建报告
除了上述的软件方向的一系列优化,也可以从硬件方向进行优化,也就是升级你的电脑配置
个人感觉影响编译速度的关键基本配置如下:
CPU
:2022年了,最好直接上M1
吧,的确要快不少,相信大家应该看到过一些说换M1
后编译速度变快的帖子CPU
更重要,因为Gradle
守护进程占用的内存可以非常大SSD
固态硬盘,256G勉强够用,最好是512G,Gradle
构建缓存(build-cache
)占用的空间也挺大的从硬件方向入手,有时也可以得到不错的优化效果,充钱是真的可以变强的
本文主要介绍了编译加速的8个实用技巧,有的接入起来非常简单,有的则需要一定的适配成本,但都是可以落地的并且有一定效果的编译加速技巧
总得来说,为了充分利用最新的优化技巧与各种新功能,我们应该及时跟进android
编译工具链的更新
大家如果想在深一步去了解不妨可以看看,有需要参考学习的可 通过此处↓↓↓,还有更多 Android 相关知识点的整理
有需要的可以复制下方链接,传送直达!!!
https://qr21.cn/CaZQLo?BIZ=ECOMMERCE