为项目添加KSP支持前需要注意的问题

jetpack room 库在 sqlite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 sql 查询的能力。它通过处理代码注解和生成 java 源代码的方式,实现上述行为。
room
https://developer.android.google.cn/training/data-storage/room
注解处理器非常强大,但它们会增加构建时间。这对于用 java 写的代码来说通常是可以接受的,但对于 kotlin 而言,编译时间消耗会非常明显,这是因为 kotlin 没有一个内置的注解处理管道。相反,它通过 kotlin 代码生成了存根 java 代码来支持注解处理器,然后将其输送到 java 编译器中进行处理。
由于并不是所有 kotlin 源代码中的内容都能用 java 表示,因此有些信息会在这种转换中丢失。同样,kotlin 是一种多平台语言,但 kapt 只在面向 java 字节码的情况下生效。
认识 kotlin 符号处理
kotlin 符号处理
https://github.com/google/ksp
随着注解处理器在 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 包) 中频繁引用的类。
auto-commonhttps://github.com/google/auto/tree/master/common
为了支持 ksp,我们有三种选择:
复制 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 clhttps://android-review.googlesource.com/c/platform/frameworks/support/+/1361181/21/room/compiler/src/main/kotlin/androidx/room/preconditions/checks.kt
改进 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)}
moreelements.astype
https://github.com/google/auto/blob/master/common/src/main/java/com/google/auto/common/moreelements.java#l131
我们把所有的调用放到了 kotlin contracts 中,这样一来就可以写成:
val element: xelement 。..if (element.istypeelement()) { // 编译器识别到元素是一个 xtypeelement}
kotlin contracts
https://kotlinlang.org/docs/whatsnew13.html#contracts
另一个很好的例子是在一个 typeelement 中找寻方法。通常在 javaap 中,您需要调用 elementfilter 类来获取 typeelement 中的方法。与此相反,我们直接将其设为 xtypeelement 中的一个属性。
// 前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
最后一个例子,这也可能是我最喜欢的例子之一,就是可分配性。在 javaap 中,如果您要检查给定的 typemirror 是否可以由另一个 typemirror 赋值,则需要调用 types.isassignable。
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-
这段代码真的很难读懂,因为您甚至无法猜到它是否验证了类型 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 的预期。
autocommon
https://github.com/google/auto/tree/master/common
由于最初的 x-processing api 是按照 avax.lang.model 建模,它们并非每次都适用于 ksp,所以我们也改进了这些 api,以便在需要时为 kotlin 提供更好的支持。
这样产生了一个新问题。现有的 room 代码库是为了处理 java 源代码而写的。当应用是由 kotlin 编写时,room 只能识别该 kotlin 在 java 存根中的样子。我们决定在 x-processing 的 ksp 实现中保持类似行为。
例如,kotlin 中的 suspend 函数在编译时生成如下签名:
// kotlinsuspend fun foo(bar:bar):baz// javaobject 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 添加 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
x-processing-testing
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/room-compiler-processing-testing/
注解处理器的编写包含 20% 的处理器代码和 80% 的测试代码。您需要考虑到各种可能的开发者错误,并确保如实报告错误消息。为了编写这些测试,room 已经提供一个辅助方法如下:
fun runtest( vararg javafileobjects: javafileobject, process: (testinvocation) -》 unit): compilationresult
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
https://github.com/google/compile-testing
糟糕的是,google compile testing 仅支持 java 源代码。为了测试 kotlin 我们需要另一个库,幸运的是有 kotlin compile testing,它允许我们编写针对 kotlin 的测试,而且我们为该库贡献了对 ksp 支持。
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
作为能让 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 集成测试便相当容易。
集成测试
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
下一步
将 ksp 支持添加到 room 只是第一步。现在,我们需要更新 room 来使用它。例如,room 中的所有类型检查都忽略了 nullability,因为 javax.lang.model 的 typemirror 并不理解 nullability。因此,当调用您的 kotlin 代码时,room 有时会在运行时触发 nullpointerexception。有了 ksp,这些检查现在可在 room 中创建新的 ksp bug (例如 b/193437407)。我们已经添加了一些临时解决方案,但理想情况下,我们仍希望改进 room 以正确处理这些情况。
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/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
更多资源
关于 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


基于FPGA开发的万年历系统
我国通信业1-10月电信业务收入累计完成了10973亿元同比增长0.2%
常见的万用表测量误差及其解决方法
视频播放不流畅怎么办?华为云CDN为你解忧!
创新微MinewSemi国产UWB模块 高精度无线定位领跑者
为项目添加KSP支持前需要注意的问题
汽车收音机射频电路设计指南 —电路图天天读(131)
2012年便携电源行业发展趋势预测及市场调查观点
人脸识别火热,GPU角色日益吃重
大数据有哪一些功能模块
小波去噪c语言程序
华为Watch D手表:真正的智能血压手表
虹科活动 | SWCF 2022卫星通信与仿真测试线上研讨会倒计时,快来报名吧!
人工智能正更新换代,中国应该有所作为
手把手教你分析一个LED驱动电源电路
5G和无线充电让手机金属CNC加速走向寒冬
芯片是什么股票板块
铅酸蓄电池企业(公司)QQ号码集合
韩国电信KT推出采用Pico G2的VR娱乐服务
充电过程中电压电流如何影响三元锂电池寿命?