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 注解处理器能够即开即用(out-of-box)地用于 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% 兼容性。