Skip to content

为什么选择 KSP

编译器插件是强大的元编程工具,可以极大地增强你的代码编写方式。 编译器插件直接将编译器作为库调用,以分析和编辑输入程序。这些插件还可以为各种用途生成输出。例如,它们可以生成样板代码,甚至可以为特殊标记的程序元素(如 Parcelable)生成完整的实现。插件还有各种其他用途,甚至可以用于实现和微调语言中未直接提供的功能。

虽然编译器插件功能强大,但这种能力并非没有代价。即使要编写最简单的插件,你也需要具备一些编译器背景知识,以及对特定编译器的实现细节有一定程度的熟悉。另一个实际问题是,插件通常与特定的编译器版本紧密关联,这意味着你可能需要每次在支持新版本编译器时更新你的插件。

KSP 使创建轻量级编译器插件更容易

KSP 旨在隐藏编译器变更,从而最大限度地减少使用它的处理器的维护工作。KSP 的设计不与 JVM 绑定,以便将来可以更容易地适应其他平台。KSP 还旨在最大限度地缩短构建时间。对于某些处理器,例如 Glide,与 kapt 相比,KSP 将完整编译时间缩短了多达 25%。

KSP 本身是作为编译器插件实现的。Google 的 Maven 仓库中提供了预构建包,你可以直接下载和使用,而无需自行构建项目。

与 kotlinc 编译器插件的比较

kotlinc 编译器插件几乎可以访问编译器中的所有内容,因此具有最大的能力和灵活性。 另一方面,由于这些插件可能依赖于编译器中的任何内容,它们对编译器变更很敏感,需要频繁维护。这些插件还需要深入理解 kotlinc 的实现,因此学习曲线可能很陡峭。

KSP 旨在通过定义良好的 API 隐藏大多数编译器变更,尽管编译器甚至 Kotlin 语言的重大变更仍可能需要暴露给 API 用户。

KSP 尝试通过提供一种以能力换取简单性的 API 来满足常见用例。它的能力是通用 kotlinc 插件的严格子集。例如,虽然 kotlinc 可以检查表达式和语句,甚至可以修改代码,但 KSP 不能。

虽然编写 kotlinc 插件会很有趣,但它也可能花费大量时间。如果你不具备学习 kotlinc 实现的条件,并且不需要修改源代码或读取表达式,那么 KSP 可能是一个很好的选择。

与反射的比较

KSP 的 API 看起来类似于 kotlin.reflect。它们之间的主要区别在于 KSP 中的类型引用需要显式解析。这是接口未共享的原因之一。

与 kapt 的比较

kapt 是一个卓越的解决方案,它使得大量的 Java 注解处理器能够开箱即用地用于 Kotlin 程序。KSP 相对于 kapt 的主要优势包括提升的构建性能、不与 JVM 绑定、更符合 Kotlin 习惯的 API 以及理解仅 Kotlin 符号的能力。

为了在不修改的情况下运行 Java 注解处理器,kapt 将 Kotlin 代码编译成 Java 存根,这些存根保留了 Java 注解处理器关心的信息。为了创建这些存根,kapt 需要解析 Kotlin 程序中的所有符号。存根生成大约需要完整 kotlinc 分析的 1/3 时间,并且与 kotlinc 代码生成的耗时处于同一数量级。对于许多注解处理器来说,这比处理器本身花费的时间要长得多。 例如,Glide 查看的是非常有限的带有预定义注解的类,并且其代码生成速度相当快。几乎所有的构建开销都存在于存根生成阶段。切换到 KSP 将立即减少编译器中花费的时间 25%。

为了进行性能评估,我们使用 KSP 为 Tachiyomi 项目实现了一个 简化版Glide 以生成代码。 在该项目在我们的测试设备上的总 Kotlin 编译时间为 21.55 秒,其中 kapt 生成代码花费了 8.67 秒,而我们的 KSP 实现生成代码花费了 1.15 秒。

与 kapt 不同,KSP 中的处理器不会从 Java 的角度看待输入程序。该 API 对 Kotlin 来说更自然,特别是对于 Kotlin 特有的功能,例如顶层函数。由于 KSP 不像 kapt 那样委托给 javac,它不假定 JVM 特有的行为,并可能与其他平台一起使用。

局限性

虽然 KSP 试图成为大多数常见用例的简单解决方案,但它与其他插件解决方案相比做出了一些权衡。 以下不是 KSP 的目标:

  • 检查源代码的表达式级别信息。
  • 修改源代码。
  • 与 Java 注解处理 API 的 100% 兼容性。