关于kotlin:Room-Kotlin-符号的处理

40次阅读

共计 7005 个字符,预计需要花费 18 分钟才能阅读完成。

△ 图片来自 Unsplash 由 Marc Reichelt 提供

Jetpack Room 库在 SQLite 上提供了一个形象层,可能在没有任何样板代码的状况下,提供编译时验证 SQL 查问的能力。它通过解决代码注解和生成 Java 源代码的形式,实现上述行为。

注解处理器十分弱小,但它们会减少构建工夫。这对于用 Java 写的代码来说通常是能够承受的,但对于 Kotlin 而言,编译工夫耗费会非常明显,这是因为 Kotlin 没有一个内置的注解解决管道。相同,它通过 Kotlin 代码生成了存根 Java 代码来反对注解处理器,而后将其输送到 Java 编译器中进行解决。

因为并不是所有 Kotlin 源代码中的内容都能用 Java 示意,因而有些信息会在这种转换中失落。同样,Kotlin 是一种多平台语言,但 KAPT 只在面向 Java 字节码的状况下失效。

意识 Kotlin 符号解决

随着注解处理器在 Android 上的宽泛应用,KAPT 成为了编译时的性能瓶颈。为了解决这个问题,Google Kotlin 编译器团队开始钻研一个代替计划,来为 Kotlin 提供一流的注解解决反对。当这个我的项目诞生之初,咱们十分冲动,因为它将帮忙 Room 更好地反对 Kotlin。从 Room 2.4 开始,它对 KSP 有了实验性的反对,咱们发现编译速度进步了 2 倍,特地是在全量编译的状况下。

本文内容重点不在注解的解决、Room 或者 KSP。而在于重点介绍咱们在为 Room 增加 KSP 反对时所面临的挑战和所做的衡量。为了了解本文您并不需要理解 Room 或者 KSP,但必须相熟注解解决。

留神: 咱们在 KSP 公布稳定版之前就开始应用它了。因而,尚不确定之前做的一些决策是否实用于当初。

本篇文章旨在让注解处理器的作者们在为我的项目增加 KSP 反对前,充沛理解须要留神的问题。

Room 工作原理简介

Room 的注解解决分为两个步骤。有一些 “Processor” 类,它们遍历用户的代码,验证并提取必要的信息到 “ 值对象 ” 中。这些值对象被送到 “Writer” 类中,这些类将它们转换为代码。和其余诸多的注解处理器一样,Room 十分依赖 Auto-Common 与 javax.lang.model 包 (Java 注解解决 API 包) 中频繁援用的类。

为了反对 KSP,咱们有三种抉择:

  1. 复制 JavaAP 和 KSP 的每个 “Processor” 类,它们会有雷同的值对象作为输入,咱们能够将其输出到 Writer 中;
  2. 在 KSP/Java AP 之上创立一个形象层,以便处理器领有一个基于该形象层的实现;
  3. 用 KSP 代替 JavaAP,并要求开发者也应用 KSP 来解决 Java 代码。

选项 C 实际上是不可行的,因为它会对 Java 用户造成重大的烦扰。随着 Room 应用数量的减少,这种破坏性的扭转是不可能的。在 “A” 和 “B” 两者之间,咱们决定抉择 “B”,因为处理器具备相当数量的业务逻辑,将其合成并非易事。

意识 X-Processing

在 JavaAP 和 KSP 上创立一个通用的形象并非易事。Kotlin 和 Java 能够互操作,但模式却不雷同,例如,Kotlin 中非凡类的类型如 Kotlin 的值类或者 Java 中的静态方法。此外,Java 类中有字段和办法,而 Kotlin 中有属性和函数。

咱们决定实现 “Room 须要什么“,而不是尝试去谋求完满的形象。从字面意思来看,在 Room 中找到导入了 javax.lang.model 的每一个文件,并将其挪动到 X-Processing 的形象中。这样一来,TypeElement 变成了 XTypeElementExecutableElemen 变成了 XExecutableElemen 等等。

遗憾的是,javax.lang.model API 在 Room 中的利用十分宽泛。一次性创立所有这些 X 类,会给审阅者带来十分重大的心理累赘。因而,咱们须要找到一种办法来迭代这一实现。

另一方面,咱们须要证实这是可行的。所以咱们首先对其做了 原型 设计,一旦验证这是一个正当的抉择,咱们就用他们本人的测试 逐个从新实现了所有 X 类。

对于我说的实现 “Room 须要什么 ”,有一个很好的例子,咱们能够在对于类的字段 更改 中看到。当 Room 解决一个类的字段时,它总是对其所有的字段感兴趣,包含父类中的字段。所以咱们在创立相应的 X-Processing API 时, 增加了获取所有字段的能力。

interface XTypeElement {fun getAllFieldsIncludingPrivateSupers(): List<XVariableElement>
}

如果咱们正在设计一个通用库,这样可能永远不会通过 API 审查。但因为咱们的指标只是 Room,并且它曾经有一个与 TypeElement 具备雷同性能的辅助办法,所以复制它能够缩小我的项目的危险。

一旦咱们有了根本的 X-Processing API 和它们的测试方法,下一步就是让 Room 来调用这个形象。这也是 “ 实现 Room 所须要的货色 ” 取得良好回报的中央。Room 在 javax.lang.model API 上曾经领有了用于基本功能的扩大函数 / 属性 (例如获取 TypeElement 的办法)。咱们首先更新了这些扩大,使其看起来与 X-Processing API 相似,而后在 1 CL 中将 Room 迁徙到 X-Processing。

改良 API 可用性

保留相似 JavaAP 的 API 并不意味着咱们不能改良任何货色。在将 Room 迁徙到 X-Processing 之后,咱们又实现了一系列的 API 改良。

例如,Room 屡次调用 MoreElement/MoreTypes,以便在不同的 javax.lang.model 类型 (例如 MoreElements.asType) 之间进行转换。相干调用通常如下所示:

val element: Element ...
if (MoreElements.isType(element)) {val typeElement:TypeElement = MoreElements.asType(element)
}

咱们把所有的调用放到了 Kotlin contracts 中,这样一来就能够写成:

val element: XElement ...
if (element.isTypeElement()) {// 编译器辨认到元素是一个 XTypeElement}

另一个很好的例子是在一个 TypeElement 中找寻办法。通常在 JavaAP 中,您须要调用 ElementFilter 类来获取 TypeElement 中的办法。与此相反,咱们间接将其设为 XTypeElement 中的一个属性。

// 前
val methods = ElementFilter.methodsIn(typeElement.enclosedElements)
// 后
val methods = typeElement.declaredMethods

最初一个例子,这也可能是我最喜爱的例子之一,就是可调配性。在 JavaAP 中,如果您要查看给定的 TypeMirror 是否能够由另一个 TypeMirror 赋值,则须要调用 Types.isAssignable。

val type1: TypeMirror ...
val type2: TypeMirror ...
if (typeUtils.isAssignable(type1, type2)) {...}

这段代码真的很难读懂,因为您甚至无奈猜到它是否验证了类型 1 能够由类型 2 指定,亦或是齐全相同的后果。咱们曾经有一个扩大函数如下:

fun TypeMirror.isAssignableFrom(
  types: Types,
  otherType: TypeMirror
): Boolean

在 X-Processing 中,咱们可能将其转换为 XType 上的惯例函数,如下方所示:

interface XType {fun isAssignableFrom(other: XType): Boolean
}

为 X-Processing 实现 KSP 后端

这些 X-Processing 接口每个都有本人的测试套件。咱们编写它们并非是用来测试 AutoCommon 或者 JavaAP 的,相同,编写它们是为了在有了它们的 KSP 实现时,咱们就能够运行测试用例来验证它是否合乎 Room 的预期。

因为最后的 X-Processing API 是依照 avax.lang.model 建模,它们并非每次都实用于 KSP,所以咱们也改良了这些 API,以便在须要时为 Kotlin 提供更好的反对。

这样产生了一个新问题。现有的 Room 代码库是为了解决 Java 源代码而写的。当利用是由 Kotlin 编写时,Room 只能辨认该 Kotlin 在 Java 存根中的样子。咱们决定在 X-Processing 的 KSP 实现中放弃相似行为。

例如,Kotlin 中的 suspend 函数在编译时生成如下签名:

// kotlin
suspend fun foo(bar:Bar):Baz
// java
Object foo(bar:Bar, Continuation<? extends Baz>)

为放弃雷同的行为,KSP 中的 XMethodElement 实现为 suspend 办法合成了一个新参数,以及新的返回类型。(KspMethodElement.kt)

留神: 这样做成果很好,因为 Room 生成的是 Java 代码,即便在 KSP 中也是如此。当咱们增加对 Kotlin 代码生成的反对时,可能会引起一些变动。

另一个例子与属性无关。Kotlin 属性也可能具备基于其签名的合成 getter/setter (拜访器)。因为 Room 冀望找到这些拜访器作为办法 (参见: KspTypeElement.kt),因而 XTypeElement 实现了这些合成办法。

留神 : 咱们已有打算更改 XTypeElement API 以提供属性而非字段,因为这才是 Room 真正想要获取的内容。正如您当初猜到的那样,咱们决定 “ 临时 ” 不这样做来缩小 Room 的批改。心愿有一天咱们可能做到这一点,当咱们这样做时,XTypeElement 的 JavaAP 实现将会把办法和字段作为属性捆绑在一起。

在为 X-Processing 增加 KSP 实现时,最初一个乏味的问题是 API 耦合。这些处理器的 API 常常互相拜访,因而如果不实现 XField / XMethod,就不能在 KSP 中实现 XTypeElement,而 XField / XMethod 自身又援用了 XType 等等。在增加这些 KSP 实现的同时,咱们为它们的实现局部写了独自的测试用例。当 KSP 的实现变得更加残缺时,咱们逐步通过 KSP 后端启动全副的 X-Processing 测试。

须要留神的是,在此阶段咱们只在 X-Processing 我的项目中运行测试,所以即便咱们晓得测试的内容没问题,咱们也无奈保障所有的 Room 测试都能通过 (也称之为单元测试 vs 集成测试)。咱们须要通过一种办法来应用 KSP 后端运行所有的 Room 测试,”X-Processing-Testing” 就应运而生。

意识 X-Processing-Testing

注解处理器的编写蕴含 20% 的处理器代码和 80% 的测试代码。您须要思考到各种可能的开发者谬误,并确保如实报告谬误音讯。为了编写这些测试,Room 曾经提供一个辅助办法如下:

runTest 在底层应用了 Google Compile Testing 库,并容许咱们简略地对处理器进行单元测试。它合成了一个 Java 注解处理器并在其中调用了处理器提供的 process 办法。

val entitySource : JavaFileObject // 示例 @Entity 正文类
val result = runTest(entitySource) { invocation ->
  val element = invocation.processingEnv.findElement("Subject")
  val entityValueObject = EntityProcessor(...).process(element)
  // 断言 entityValueObject
}
// 断言后果是否有误,正告等

蹩脚的是,Google Compile Testing 仅反对 Java 源代码。为了测试 Kotlin 咱们须要另一个库,侥幸的是有 Kotlin Compile Testing,它容许咱们编写针对 Kotlin 的测试,而且咱们为该库奉献了对 KSP 反对。

留神 : 咱们起初用 外部实现 替换了 Kotlin Compile Testing,以简化 AndroidX Repo 中的 Kotlin/KSP 更新。咱们还增加了更好的断言 API,这须要咱们对 KCT 执行 API 不兼容的批改操作。

作为能让 KSP 运行所有测试的最初一步,咱们创立了以下测试 API:

fun runProcessorTest(
  sources: List<Source>,
  handler: (XTestInvocation) -> Unit
): Unit

这个和原始版本之间的次要区别在于,它同时通过 KSP 和 JavaAP (或 KAPT,取决于起源) 运行测试。因为它屡次运行测试且 KSP 和 JavaAP 两者的判断后果不同,因而无奈返回单个后果。

因而,咱们想到了一个方法:

fun XTestInvocation.assertCompilationResult(assertion: (XCompilationResultSubject) -> Unit
}

每次编译后,它都会调用后果断言 (如果没有失败提醒,则查看编译是否胜利)。咱们把每个 Room 测试重构为如下所示:

val entitySource : Source // 示例 @Entity 正文类
runProcessorTest(listOf(entitySource)) { invocation ->
  // 该代码块运行两次,一次应用 JavaAP/KAPT,一次应用 KSP
  val element = invocation.processingEnv.findElement("Subject")
  val entityValueObject = EntityProcessor(...).process(element)
  //  断言 entityValueObject
  invocation.assertCompilationResult {
    // 后果被断言为是否有 error,warning 等
    hasWarningContaining("...")
  }
}

接下来的事件就很简略了。将每个 Room 的编译测试迁徙到新的 API,一旦发现新的 KSP / X-Processing 谬误,就会上报,而后施行长期解决方案;这一动作重复进行。因为 KSP 正在鼎力开发中,咱们的确遇到了很多 bug。每一次咱们都会上报 bug,从 Room 源链接到它,而后继续前进 (或者进行修复)。每当 KSP 公布之后,咱们都会搜寻代码库来找到已修复的问题,删除长期解决方案并启动测试。

一旦编译测试笼罩状况较好,咱们在下一步就会应用 KSP 运行 Room 的 集成测试。这些是理论的 Android 测试利用,也会在运行时测试其行为。侥幸的是,Android 反对 Gradle 变体,因而应用 KSP 和 KAPT 来运行咱们 Kotlin 集成测试 便相当容易。

下一步

将 KSP 反对增加到 Room 只是第一步。当初,咱们须要更新 Room 来应用它。例如,Room 中的所有类型查看都疏忽了 nullability,因为 javax.lang.modelTypeMirror 并不了解 nullability。因而,当调用您的 Kotlin 代码时,Room 有时会在运行时触发 NullPointerException。有了 KSP,这些查看当初可在 Room 中创立新的 KSP bug (例如 b/193437407)。咱们曾经增加了一些长期解决方案,但现实状况下,咱们仍心愿 改良 Room 以正确处理这些状况。

同样,即便咱们反对 KSP,Room 依然只生成 Java 代码。这种限度使咱们无奈增加对某些 Kotlin 个性的反对,比方 Value Classes。心愿在未来,咱们还能对生成 Kotlin 代码提供一些反对,以便在 Room 中为 Kotlin 提供一流的反对。接下来,兴许更多 :)。

我能在我的我的项目上应用 X-Processing 吗?

答案是还不能;至多与您应用任何其余 Jetpack 库的形式不同。如前文所述,咱们只实现了 Room 须要的局部。编写一个真正的 Jetpack 库有很大的投入,比方文档、API 稳定性、Codelabs 等,咱们无奈承当这些工作。话虽如此,Dagger 和 Airbnb (Paris、DeeplinkDispatch) 都开始用 X-Processing 来反对 KSP (并奉献了他们须要的货色🙏)。兴许有一天咱们会把它从 Room 中合成进去。从技术层面上讲,您依然能够像应用 Google Maven 库 一样应用它,然而没有 API 保障能够这样做,因而您相对应该应用 shade 技术。

总结

咱们为 Room 增加了 KSP 反对,这并非易事但相对值得。如果您在保护注解处理器,请增加对 KSP 的反对,以提供更好的 Kotlin 开发者体验。

特别感谢 Zac Sweers 和 Eli Hart 审校这篇文章的晚期版本,他们同时也是优良的 KSP 贡献者。

更多资源

  • 对于 Room 对于 KSP 反对的 Issue Tracker
  • X-Processing 源码
  • X-Processing-Testing 源码
  • KSP 源码

欢迎您 点击这里 向咱们提交反馈,或分享您喜爱的内容、发现的问题。您的反馈对咱们十分重要,感谢您的反对!

正文完
 0