Android编译解析-Kapt

2022-05-10 20:53:01 浏览数 (1)

正题

上篇说到 Android 在编译过程中发起 kotlin 编译的流程。今天分享一下关于 Kotlin 编译里面比较重要的相关步骤:kapt。在 kotlin 里,需要使用 apt 的话,需要使用 kapt 插件来替代 annotationProcessor 的声明:

代码语言:javascript复制
apply plugin: 'kotlin-kapt'
  
// 依赖
kat 'xx.xx:xxx'  

在编译过程中编译任务会变成下面这样:

kapt 分成两部

generateStubs

生成 stubs 文件, 因为 apt 无法识别 Kotlin 语法,所以 kapt 第一步需要把 Kotlin 文件转换成 apt 可以识别出来的 Java 文件。

kapt 需要把 kotlin 文件生成一个 Java 编译可以认识的产物。但是直接翻译成 Java 文件是没有必要的,这里会生成一个和 kotlin 类类名、字段、方法签名一样但是没有具体实现的 Java 文件。Java文件存在module/build/tmp/kapt3/stubs/这个目录里面。

例如我们写一个 Kotlin 类:

代码语言:javascript复制
class AKtDog {
    private val TAG = "AKtDog"

    fun playWith(age: Int): Int {
        println(age)
        println("2222")
        println("1111")
        return 11
    }
}

kapt会生成这个 Kotlin 类的 stub 文件:

代码语言:javascript复制
import java.lang.System;

@kotlin.Metadata
public final class AKtDog {
    private final java.lang.String TAG = "AKtDog";
    
    public AKtDog() {
        super();
    }
    
    public final int playWith(int age) {
        return 0;
    }
}

这里生成的 Java 文件的 playWith 方法里其实是没有逻辑的,所以只是一个壳,把类的 abi 提供给 apt。

这个 task 的源码可以在 kotlin 的源码里面找到: org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask

这个 Task 继承自 org.jetbrains.kotlin.gradle.tasks.KotlinCompiler, 说明生成 stubs 的流程其实是遵循了 Kotlin编译的过程的。

这里调用 Kotlin 编译的时候会带入 kapt 相关的命令行参数:-Xplugin=xx/xx/xx/kotlin-annotation-processing-gradle-x.x.x-sources.jar

generateStubs 也支持增量编译,相关的编译缓存内容也和 Koltin编译一样:

kapt build

kotlin apt 编译,执行 apt 流程生成代码。

那么 apt 是咋触发的呢,这个也可以在 Kotlin 的源码里面找到。这部分的 task 源码在 org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask里面

在它的 @TaskAction 里面会调用 kapt 的任务:org.jetbrains.kotlin.gradle.internal.KaptExecution:

代码语言:javascript复制
// 定义 kapt class
private fun kaptClass(classLoader: ClassLoader) = Class.forName("org.jetbrains.kotlin.kapt3.base.Kapt", true, classLoader)
val kaptMethod = kaptClass(kaptClassLoader).declaredMethods.single { it.name == "kapt" }
kaptMethod.invoke(null, createKaptOptions(kaptClassLoader))

这里执行的是 org.jetbrains.kotlin.kapt3.base.Kapt对象的 kapt 方法。这个里面就会和 Java 编译一样去调用我们的 apt 了:

代码语言:javascript复制
val annotationProcessingTime = measureTimeMillis {
  kaptContext.doAnnotationProcessing(
    javaSourceFiles,
    processors.processors,
    binaryTypesToReprocess = collectAggregatedTypes(kaptContext.sourcesToReprocess)
  )
}

禁用kapt

通过上面的分析,我们可以发现,其实 apt 本身支持了增量编译,编译速度还是非常快的。

但是kapt生成stubs文件的流程在增量编译的时候就不是那么可控了,和 kotlin 编译一样,会有各种case让增量失效。那么全量重新生成一遍 stubs 文件那编译速度就有点慢了。

不过转念一想,我们是不是可以增量编译的时候在没有 apt 相关注解变动的时候直接禁用 generateStubs 这个 task呢?理论上这么做是可以的,所以我进行了一下尝试,在 gradle plugin 里面去寻找 generateStubs 的任务,然后禁用这个任务。最后我这里试验是成功的。具体做法如下:

代码语言:javascript复制
project.tasks.whenTaskAdded {
  if (it.name.contains("kaptGenerateStubs")) {
    it.enabled = false
  }
}

这里我们把 KaptGenerateStubs 相关的 task 的 enabled设置为 false,最后编译的时候,这个task则会跳过,控制台的输出如下:

代码语言:javascript复制
> Task :amodule:kaptGenerateStubsDebugKotlin SKIPPED

总结

今天分享了一下 kapt 相关的内容,我们可以从中相关的内容了解 kapt 的原理。在未来我们可以及时拥抱 ksp 等新技术,直接在 kotlin 的 AST 中处理,来提升我们的编译效率。

0 人点赞