本文原作者: 程序员江同学原文发布于: 程序员江同学

关于 Android 编译加速的文章相信大家都看过不少,今天我们就一起来看看,在 AGP7.0 时代,除了传统的开启 build-cache,打开并行编译,调整 Gradle 堆内存大小等常用手段之外,还有哪些可以落地的编译加速实用技巧。

使用最新版本编译工具链

几乎每次更新时,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 在性能上与易用性上都有一定的提升。

KAPT 迁移到 KSP

注解处理器是 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 已经发布了稳定版了,像 Room,Moshi 等库也已经做了适配,对于这些已经适配了的库,我们可以直接迁移。

但还是有一些常用的库比如 Glide,ARouter 还没有做适配,这些库是我们移除 KAPT 最大的障碍。

下面给出一些还不支持 KSP 的库的过渡迁移方法:

  1. KAPT 一般就是用来生成代码,像 Glide 这种生成的代码比较稳定的库 (通常只有几个 @GlideModule),可以直接把生成的代码从 build 目录拷贝到项目中来,然后移除 KAPT,后续如果有新的 @GlideModule 再更新下生成的文件 (当然这样可能不太方便,只是一种过渡的方式,等待 Glide 官方更新);

  2. 对于 ARouter 这种生成代码不断增加的库 (不断有新的 @ARouter 注解),上面的方式就不太适用了。考虑到 ARoutr 已经很久没有更新了,可以考虑迁移到一个不使用 KAPT 的新的路由库。

更新: Glide 最新版本已经支持了 KSP,可以直接升级接入了。

开启 Configuration Cache

我们知道,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 适配

当然打开 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 耗时产生比较大的影响。

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 之后,可以一步步开始迁移了:

  1. 检测库有没有已经支持了 androidX 的最新版本,如果有直接升级即可;

  2. 如果有源码,添加 android.useAndroidX = true,迁移到 AndroidX,然后升级所有的依赖,发布个新版本就可以了;

  3. 如果没有源码,或对于您的项目来说,它太老了。可以用 jetifier-standalone 命令行工具把 AAR/JAR 转成 jetified 之后的 AAR/JAR。这个命令行的转换效果和您在代码里开启 android.enableJetifier 的效果是一样的。命令行如下:

// https://developer.android.com/studio/command-line/jetifier
./jetifier-standalone -i <source-library> -o <output-library>

关闭 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 文件都需要重新编译。

  1. 关闭 R 文件传递可以通过编译避免的方式获得更快的编译速度;

  2. 关闭 R 文件传递有助于确保每个模块的 R 类仅包含对其自身资源的引用,避免无意中引用其他模块资源,明确模块边界;

  3. 关闭 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: 最好直接上 M1 吧,的确要快不少,相信大家应该看到过一些说换 M1 后编译速度变快的帖子;

  • 内存: 至少要 16G,有条件建议上 32G,对于一些大型项目,内存甚至比 CPU 更重要,因为 Gradle 守护进程占用的内存可以非常大;

  • 硬盘: 必须是 SSD 固态硬盘,256G 勉强够用,最好是 512G,Gradle 构建缓存 (build-cache) 占用的空间也挺大的。

从硬件方向入手,有时也可以得到不错的优化效果。

总结

本文主要介绍了编译加速的 8 个实用技巧,有的接入起来非常简单,有的则需要一定的适配成本,但都是可以落地的并且有一定效果的编译加速技巧。

总得来说,为了充分利用最新的优化技巧与各种新功能,我们应该及时跟进 Android 编译工具链的更新。

更多内容可查看:

  • How we reduced our Gradle build times by over 80%

    https://medium.com/proandroiddev/how-we-reduced-our-gradle-build-times-by-over-80-51f2b6d6b05b

  • 10 ideas to improve your Gradle build times

    https://blog.dipien.com/10-great-ideas-to-improve-your-gradle-build-times-2a6b281c69c6


长按右侧二维码

查看更多开发者精彩分享

"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。

 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"


编译加速的 8 个实用技巧 | 开发者说·DTalk相关推荐

  1. APICloud开发者进阶之路 | 超级实用技巧

    金秋9月,脑海还是北京的"中非合作蓝"!今天按照惯例,柚子君还是要督促大家充充电的! 本周<30天,App开发从0到1>将要给大家分享第十章超级实用技巧,我们精选了其中 ...

  2. 经典 | 深度学习的7大实用技巧

    编译 | AI科技大本营 参与 | 林椿眄 编辑 | 谷 磊 对于许多具有挑战性的现实问题,深度学习已经成为最有效的解决方法. 例如,对于目标检测,语音识别和语言翻译等问题,深度学习能够表现出最佳的性 ...

  3. Vim 实用技术,第 1 部分: 实用技巧

    0. Vim 简介 作为开源世界最重要的编辑器之一(另一个是 Emacs),Vim 以其强大的功能和可定制能力被众多开发者所喜爱.不过,也许就是因为 Vim 的功能太强大了,要真正用好 Vim 并不容 ...

  4. 实用技巧:使用 Google Analytics 跟踪 JS 错误

    Google Analytics(谷歌分析)不仅仅是一个流量统计工具,你还可以用它来测量广告活动的有效性,跟踪用户多远到所需的页面流(从点击广告到购物车到结账页面)获取,并基于用户的信息设置浏览器和语 ...

  5. Vim 实用技术,第 1 部分: 实用技巧(转)

    原文链接:http://blog.jobbole.com/20604/ 0. Vim 简介 作为开源世界最重要的编辑器之一(另一个是 Emacs),Vim 以其强大的功能和可定制能力被众多开发者所喜爱 ...

  6. 那些你可能不知道的谷歌浏览器实用技巧

    苏生不惑第155 篇原创文章,将本公众号设为星标,第一时间看最新文章. 关于谷歌浏览器之前写过以下文章: 实用油猴脚本推荐,让你的谷歌浏览器更强大 Chrome 浏览器扩展神器油猴 请停用以开发者模式 ...

  7. 为数不多的人知道的 Kotlin 技巧以及原理解析 | 开发者说·DTalk

    本文原作者: HiDhl,原文发布于: 掘金 https://juejin.im/post/6847902224467623950 文章中没有奇淫技巧,都是一些在实际开发中常用,但很容易被我们忽略的一 ...

  8. 深度学习11个实用技巧

    深度学习11个实用技巧 深度学习工程师George Seif发表了一篇博文,总结了7个深度学习的技巧,本文增加了几个技巧,总结了11个深度学习的技巧,主要从提高深度学习模型的准确性和速度两个角度来分析 ...

  9. 5 个针对有经验用户的 Vim 实用技巧

    Vim 编辑器提供了很多的特性,要想全部掌握它们很困难.然而,花费更多的时间在命令行编辑器上总是有帮助的.毫无疑问,和 Vim 用户们进行交流能够让你更快地学习新颖有创造性的东西. 注: 本文中用到的 ...

最新文章

  1. Node.js v0.10版本发布
  2. python的with用法(参考)
  3. 获取当前目录所有文件名 并且保存为1个00000.txt的文件文件bat
  4. [转]JavaScript var obj = { id:1, name:jacky } 大括号是啥意思?
  5. Python | Pyplot标签
  6. vue选中点击的元素_vue中v-for循环选中点击的元素并对该元素添加样式操作
  7. XPath 获取两个node中间的HTML Nodes
  8. 动态规划--凑硬币问题
  9. Fortran入门教程(十)——结构体
  10. java sql拼接字符串_java字符串拼接(SQL)
  11. 机器学习笔记之二十一——基于统计的中文分词方法
  12. css特殊符号编码大全
  13. 网站优化之sitemap.xml网站地图的写法
  14. 12_传智播客iOS视频教程_注释和函数的定义和调用
  15. Centos7.4-docker安装部署实战
  16. 一个测试工程师走进一家酒吧……
  17. 智慧家庭产业链及典型企业
  18. 什么是数据中心基础设施管理(DCIM)
  19. EXCEL表格-下拉列表的创建与基础函数联动
  20. k8s重新生成token

热门文章

  1. bzoj 1673(A*)
  2. codeforce1673C. Palindrome Basis
  3. PHP SQL防注入
  4. 民间家具的几种木材及收藏的几个坑
  5. x3250m6服务器光驱怎么找不到,IBM System x3250M2服务器常见问题解答
  6. php验证日语汉字平假名片假名、全半数字半角英文输入
  7. Serializable
  8. 车辆工程常用英文缩写
  9. MySQL表空间详解
  10. 树莓派 Pi Camera: out of resorce error