△ 图片来自 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 工作原理简介
选项 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>
}
如果我们正在设计一个通用库,这样可能永远不会通过 API 审查。但因为我们的目标只是 Room,并且它已经有一个与 TypeElement 具有相同功能的辅助方法,所以复制它可以减少项目的风险。
一旦我们有了基本的 X-Processing API 和它们的测试方法,下一步就是让 Room 来调用这个抽象。这也是 "实现 Room 所需要的东西" 获得良好回报的地方。Room 在 javax.lang.model API 上已经拥有了用于基本功能的扩展函数/属性 (例如获取 TypeElement 的方法)。我们首先更新了这些扩展,使其看起来与 X-Processing API 类似,然后在 1 CL 中将 Room 迁移到 X-Processing。
改进 API 可用性
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
interface XType {
fun isAssignableFrom(other: XType): Boolean
}
为 X-Processing 实现 KSP 后端
AutoCommon
https://github.com/google/auto/tree/master/common
// kotlin
suspend fun foo(bar:Bar):Baz
// java
Object foo(bar:Bar, Continuation<? extends Baz>)
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 代码生成的支持时,可能会引起一些变化。
注意 : 我们已有计划更改 XTypeElement API 以提供 属性 而非 字段 ,因为这才是 Room 真正想要获取的内容。正如您现在猜到的那样,我们决定 "暂时" 不这样做来减少 Room 的修改。希望有一天我们能够做到这一点,当我们这样做时, XTypeElement 的 JavaAP 实现将会把方法和字段作为属性捆绑在一起。
认识 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("...")
}
}
下一步
b/193437407
https://issuetracker.google.com/issues/193437407
改进
https://android-review.googlesource.com/c/platform/frameworks/support/+/1844471
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/paris
DeeplinkDispatch
https://github.com/airbnb/DeepLinkDispatch
Google Maven 库
https://maven.google.com/web/index.html#androidx.room
shade
https://github.com/johnrengelman/shadow
总结
我们为 Room 添加了 KSP 支持,这并非易事但绝对值得。如果您在维护注解处理器,请添加对 KSP 的支持,以提供更好的 Kotlin 开发者体验。
特别感谢 Zac Sweers 和 Eli Hart 审校这篇文章的早期版本,他们同时也是优秀的 KSP 贡献者。
Zac Sweers
https://medium.com/@ZacSweers
Eli Hart
https://medium.com/@konakid
更多资源
https://issuetracker.google.com/issues/160322705
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing/
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing-testing/
https://github.com/google/ksp
免费中文系列课程下载
系统地学习使用 Kotlin 进行 Android 开发
☟ 即刻了解课程详情 ☟
推荐阅读