深入理解Android编译-kapt篇

1,169 阅读3分钟

正题

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

apply plugin: 'kotlin-kapt'
  
// 依赖
kat 'xx.xx:xxx'  

在编译过程中编译任务会变成下面这样: image.png kapt 分成两部

generateStubs

生成 stubs 文件。

因为 apt 无法识别 Kotlin 语法,所以 kapt 第一步需要把 Kotlin 文件转换成 apt 可以识别出来的 Java 文件。所以 kapt 需要把 kotlin 文件生成一个 Java 编译可以认识的产物。

但是直接生成 Java 文件是没有必要的,所以这里会生成一个和 kotlin 类类名、字段、方法签名一样的 Java 文件。 Java文件存在 module/build/tmp/kapt3/stubs/ 这个目录里面。

例如我们写一个 Kotlin 类:

class AKtDog {
    private val TAG = "AKtDog"

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

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

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 image.png generateStubs 也支持增量编译,相关的编译缓存内容也和 Koltin编译一样: image.png

kapt build

kotlin apt 编译,执行 apt 流程生成代码。那么 apt 是咋触发的呢,也可以在 Kotlin 的源码里面找到。 这部分的 task 源码在 org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask 里面,在它的 @TaskAction 里面会调用 kapt 的任务: org.jetbrains.kotlin.gradle.internal.KaptExecution:

// 定义 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 了:

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 的任务,然后禁用这个任务。最后我这边试验是成功的。具体做法如下:

project.tasks.whenTaskAdded {
  if (it.name.contains("kaptGenerateStubs")) {
    it.enabled = false
  }
}

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

> Task :amodule:kaptGenerateStubsDebugKotlin SKIPPED

总结

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