一、背景
本文是编译优化系列文章之 kapt 优化篇,后续还会有 build cache, kotlin, dex 优化等文章,敬请期待。本文由Client Infra->Build Infra团队出品,powered by 王龙海,封光,兰军健
相信 android 开发对于 kapt 并不陌生,之前也有很多文章在编译优化过程中谈及过 Kapt,主要是针对增量编译场景。
抖音火山版同学在接入 hilt 过程中,遇到了更严重的问题: 在 16G 内存的电脑上触发 OOM。例如火山项目在执行 kapt 的过程中,不论采用 aar 依赖,还是全源码编译,均无法编译通过,可以认为 Kapt 会对内存产生比较大的影响。
在分析这个问题之前,先介绍下 kapt 的原理。
二、 Kapt 原理
kapt 可以理解为就是在 kotlin 开发场景下进行注解处理的工具。至于作用可以完全等效于 java 的 apt。因为 java 的 apt 处理不了 kotlin 源码文件,所以才出现了kapt,来实现混合工程或者纯 kotlin 工程的 apt 任务。
使用起来非常简单:
你只需要引入 kapt 插件,将原来的 annotationProcessor
换成 kapt
,即可让 kotlin 帮你完成原来 apt 的工作。
kapt "groupId:artifactId:version"
apply plugin: 'kotlin-kapt'
当你在某个 module 下引入了 ‘kotlin-kapt’,相应的模块构建过程中就会自动生成kaptGenerateStu``b``s``${variant}k``otlin
和kapt``${variant}``Kotlin
两个Task。所以要最小化引入原则,按需引入,避免带来较大的编译耗时影响。
上文说到引入了 kapt 的模块会相应的增加两个 Task,这两个 Task 会完成处理注解生成类的功能。接下来我们简单的看一下这两个 Task 的工作原理。
这里可以看到,整个 kapt 的处理过程分为了两个步骤:“生成 Stub 文件"及"调用 apt 处理注解”。可以非常清晰的看到,其实kapt并没有新的东西,底层依然是调用的 java apt 来完成的整个任务。这里多说一句,
kotlin 团队为什么这么设计呢?
Java 的 apt 是通过实现 JSR269 来实现的。JSR269 为 apt 插件定义了 api,Java apt 实现了这套 api。
那么作为后起之秀,想要实现类似的功能可以很容易想到如下两种方式:
- 重写一套 JSR269 api。同时实现对于 kotlin 文件和 java 文件的 apt
- 想办法复用 java 的 apt
显然,第二种路径更简单且更成熟,再加上在 kotlin 考虑这件事之前,业界已有先例,比如 groovy 对于 apt 的支持也是这么干的。这就不难理解 kotlin 的设计思路了,只要想办法把 kotlin 的源码转成 java 源码即可。
到这里就不难理解 kapt 的处理为什么分为了两个步骤:"生成 Stub 文件"及"调用 apt 处理注解"了。
下面说一下这两个步骤的大致流程。
生成Stub文件
这个过程由kaptGenerateStubs${variant}kotlin
承担。 如上图所示,A.kt 和 B.kt 经过处理后生成了 A.java 和 B.java。我们来看一下产物和我们想象的是否有不同之处。
左边是一个 .kt 文件,右边是 kaptGenetateStub 生成的 .java 文件,聪明的你应该知道 kotlin 想干嘛了吧?
可以看到