Room & Kotlin 符号的处理
△ 图片来自 Unsplash
Jetpack Room 库在 SQLite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 SQL 查询的能力。它通过处理代码注解和生成 Java 源代码的方式,实现上述行为。
Room
https://developer.android.google.cn/training/data-storage/room
认识 Kotlin 符号处理
Kotlin 符号处理
https://github.com/google/ksp
注意: 我们在 KSP 发布稳定版之前就开始使用它了。因此,尚不确定之前做的一些决策是否适用于现在。
Room 工作原理简介
Auto-Common https://github.com/google/auto/tree/master/common
复制 JavaAP 和 KSP 的每个 "Processor" 类,它们会有相同的值对象作为输出,我们可以将其输入到 Writer 中; 在 KSP/Java AP 之上创建一个抽象层,以便处理器拥有一个基于该抽象层的实现; 用 KSP 代替 JavaAP,并要求开发者也使用 KSP 来处理 Java 代码。
选项 C 实际上是不可行的,因为它会对 Java 用户造成严重的干扰。随着 Room 使用数量的增加,这种破坏性的改变是不可能的。在 "A" 和 "B" 两者之间,我们决定选择 "B",因为处理器具有相当数量的业务逻辑,将其分解并非易事。
认识 X-Processing
X-Processing
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/
在 JavaAP 和 KSP 上创建一个通用的抽象并非易事。Kotlin 和 Java 可以互操作,但模式却不相同,例如,Kotlin 中特殊类的类型如 Kotlin 的值类或者 Java 中的静态方法。此外,Java 类中有字段和方法,而 Kotlin 中有属性和函数。
我们决定实现 "Room 需要什么",而不是尝试去追求完美的抽象。从字面意思来看,在 Room 中找到导入了 javax.lang.model 的每一个文件,并将其移动到 X-Processing 的抽象中。这样一来,TypeElement 变成了 XTypeElement,ExecutableElemen 变成了 XExecutableElemen 等等。
遗憾的是,javax.lang.model API 在 Room 中的应用非常广泛。一次性创建所有这些 X 类,会给审阅者带来非常严重的心理负担。因此,我们需要找到一种方法来迭代这一实现。
另一方面,我们需要证明这是可行的。所以我们首先对其做了原型设计,一旦验证这是一个合理的选择,我们就用他们自己的测试逐一重新实现了所有 X 类。
原型
https://android-review.googlesource.com/c/platform/frameworks/support/+/1362062逐一重新实现了所有 X 类
https://android-review.googlesource.com/c/platform/frameworks/support/+/1362102
关于我说的实现 "Room 需要什么",有一个很好的例子,我们可以在关于类的字段更改中看到。当 Room 处理一个类的字段时,它总是对其所有的字段感兴趣,包括父类中的字段。所以我们在创建相应的 X-Processing API 时,只添加了获取所有字段的能力。
interface XTypeElement {
fun getAllFieldsIncludingPrivateSupers(): List<XVariableElement>
}
更改 https://android-review.googlesource.com/c/platform/frameworks/support/+/1362165/6/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/JavacTypeElement.kt
如果我们正在设计一个通用库,这样可能永远不会通过 API 审查。但因为我们的目标只是 Room,并且它已经有一个与 TypeElement 具有相同功能的辅助方法,所以复制它可以减少项目的风险。
一旦我们有了基本的 X-Processing API 和它们的测试方法,下一步就是让 Room 来调用这个抽象。这也是 "实现 Room 所需要的东西" 获得良好回报的地方。Room 在 javax.lang.model API 上已经拥有了用于基本功能的扩展函数/属性 (例如获取 TypeElement 的方法)。我们首先更新了这些扩展,使其看起来与 X-Processing API 类似,然后在 1 CL 中将 Room 迁移到 X-Processing。
1 CL https://android-review.googlesource.com/c/platform/frameworks/support/+/1361181/21/room/compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
改进 API 可用性
例如,Room 多次调用 MoreElement/MoreTypes,以便在不同的 javax.lang.model 类型 (例如 MoreElements.asType) 之间进行转换。相关调用通常如下所示:
val element: Element ...
if (MoreElements.isType(element)) {
val typeElement:TypeElement = MoreElements.asType(element)
}
MoreElements.asType
https://github.com/google/auto/blob/master/common/src/main/java/com/google/auto/common/MoreElements.java#L131
val element: XElement ...
if (element.isTypeElement()) {
// 编译器识别到元素是一个 XTypeElement
}
Kotlin contracts
https://kotlinlang.org/docs/whatsnew13.html#contracts
// 前
val methods = ElementFilter.methodsIn(typeElement.enclosedElements)
// 后
val methods = typeElement.declaredMethods
ElementFilter
https://docs.oracle.com/javase/7/docs/api/javax/lang/model/util/ElementFilter.html
val type1: TypeMirror ...
val type2: TypeMirror ...
if (typeUtils.isAssignable(type1, type2)) {
...
}
Types.isAssignable
https://docs.oracle.com/javase/8/docs/api/javax/lang/model/util/Types.html#isAssignable-javax.lang.model.type.TypeMirror-javax.lang.model.type.TypeMirror-
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 的预期。
AutoCommon
https://github.com/google/auto/tree/master/common
这样产生了一个新问题。现有的 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)
KspMethodElement.kt
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt;l=108?q=KspSuspendMethodElement&ss=androidx
注意: 这样做效果很好,因为 Room 生成的是 Java 代码,即使在 KSP 中也是如此。当我们添加对 Kotlin 代码生成的支持时,可能会引起一些变化。
另一个例子与属性有关。Kotlin 属性也可能具有基于其签名的合成 getter/setter (访问器)。由于 Room 期望找到这些访问器作为方法 (参见: KspTypeElement.kt),因此 XTypeElement 实现了这些合成方法。
KspTypeElement.kt
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt;l=144
注意: 我们已有计划更改 XTypeElement API 以提供属性而非字段,因为这才是 Room 真正想要获取的内容。正如您现在猜到的那样,我们决定 "暂时" 不这样做来减少 Room 的修改。希望有一天我们能够做到这一点,当我们这样做时,XTypeElement 的 JavaAP 实现将会把方法和字段作为属性捆绑在一起。
需要注意的是,在此阶段我们只在 X-Processing 项目中运行测试,所以即使我们知道测试的内容没问题,我们也无法保证所有的 Room 测试都能通过 (也称之为单元测试 vs 集成测试)。我们需要通过一种方法来使用 KSP 后端运行所有的 Room 测试,"X-Processing-Testing" 就应运而生。
认识 X-Processing-Testing
X-Processing-Testing
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing-testing/
fun runTest(
vararg javaFileObjects: JavaFileObject,
process: (TestInvocation) -> Unit
): CompilationResult
val entitySource : JavaFileObject //示例 @Entity 注释类
val result = runTest(entitySource) { invocation ->
val element = invocation.processingEnv.findElement("Subject")
val entityValueObject = EntityProcessor(...).process(element)
// 断言 entityValueObject
}
// 断言结果是否有误,警告等
Google Compile Testing
https://github.com/google/compile-testing
Kotlin Compile Testing
https://github.com/tschuchortdev/kotlin-compile-testing
注意: 我们后来用内部实现替换了 Kotlin Compile Testing,以简化 AndroidX Repo 中的 Kotlin/KSP 更新。我们还添加了更好的断言 API,这需要我们对 KCT 执行 API 不兼容的修改操作。
内部实现
https://android-review.googlesource.com/c/platform/frameworks/support/+/1779266
fun runProcessorTest(
sources: List<Source>,
handler: (XTestInvocation) -> Unit
): Unit
这个和原始版本之间的主要区别在于,它同时通过 KSP 和 JavaAP (或 KAPT,取决于来源) 运行测试。因为它多次运行测试且 KSP 和 JavaAP 两者的判断结果不同,因此无法返回单个结果。
因此,我们想到了一个办法:
fun XTestInvocation.assertCompilationResult(
assertion: (XCompilationResultSubject) -> Unit
}
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 集成测试便相当容易。
集成测试
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/integration-tests/Kotlin 集成测试
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/integration-tests/kotlintestapp/build.gradle
下一步
b/193437407
https://issuetracker.google.com/issues/193437407改进
https://android-review.googlesource.com/c/platform/frameworks/support/+/1844471
同样,即使我们支持 KSP,Room 仍然只生成 Java 代码。这种限制使我们无法添加对某些 Kotlin 特性的支持,比如 Value Classes。希望在将来,我们还能对生成 Kotlin 代码提供一些支持,以便在 Room 中为 Kotlin 提供一流的支持。接下来,也许更多 :)。
Value Classes
https://kotlinlang.org/docs/inline-classes.html
我能在我的项目上使用 X-Processing 吗?
答案是还不能;至少与您使用任何其他 Jetpack 库的方式不同。如前文所述,我们只实现了 Room 需要的部分。编写一个真正的 Jetpack 库有很大的投入,比如文档、API 稳定性、Codelabs 等,我们无法承担这些工作。话虽如此,Dagger 和 Airbnb (Paris、DeeplinkDispatch) 都开始用 X-Processing 来支持 KSP (并贡献了他们需要的东西🙏)。也许有一天我们会把它从 Room 中分解出来。从技术层面上讲,您仍然可以像使用 Google Maven 库一样使用它,但是没有 API 保证可以这样做,因此您绝对应该使用 shade 技术。
Paris
https://github.com/airbnb/parisDeeplinkDispatch
https://github.com/airbnb/DeepLinkDispatchGoogle Maven 库
https://maven.google.com/web/index.html#androidx.roomshade
https://github.com/johnrengelman/shadow
总结
我们为 Room 添加了 KSP 支持,这并非易事但绝对值得。如果您在维护注解处理器,请添加对 KSP 的支持,以提供更好的 Kotlin 开发者体验。
特别感谢 Zac Sweers 和 Eli Hart 审校这篇文章的早期版本,他们同时也是优秀的 KSP 贡献者。
Zac Sweers
https://medium.com/@ZacSweersEli Hart
https://medium.com/@konakid
更多资源
关于 Room 对于 KSP 支持的 Issue Tracker https://issuetracker.google.com/issues/160322705
X-Processing 源码 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/
X-Processing-Testing 源码 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing-testing/
KSP 源码 https://github.com/google/ksp
免费中文系列课程下载
系统地学习使用 Kotlin 进行 Android 开发
☟ 即刻了解课程详情 ☟
推荐阅读