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 中实现了一个 简化版Glide,使其为 Tachiyomi 项目生成代码。虽然该项目在我们的测试设备上的全量 Kotlin 编译时间为 21.55 秒,但 kapt 生成代码耗时 8.67 秒,而我们的 KSP 实现生成代码仅耗时 1.15 秒。

与 kapt 不同,KSP 中的处理器不会从 Java 的角度看待输入程序。其 API 对 Kotlin 来说更加自然,尤其是对于像顶级函数这样的 Kotlin 特有功能。由于 KSP 不像 kapt 那样委托给 javac,它不假设 JVM 特有的行为,因此潜力上可以用于其他平台。

局限性

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

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