开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情
续接上文: KSP准备好大规模应用了吗? - 1
什么是KAPT?
KAPT(Kotlin注释处理工具)将处理分为两个步骤:
- 从Kotlin源文件(包含处理所需的所有内容)生成存根Java类.
- 合并所有的源文件, 包括存根, 将其传递给编译器.
简单地说, KAPT内部与Java源文件一起工作. 因此, 同样的注解处理器被用于.
Stub的生成增加了编译时间, 新生成的源代码必须用Java编写, 并兼容Kotlin类型.
增量构建
从Gradle插件4.7开始, 编译器也支持增量注解处理.
有2种类别:
- 隔离类型
- 聚合类型
此外, 还有dynamic模式, 处理器通过getSupportedOptions方法定义将使用哪个类别.
一个隔离式处理器在隔离中处理每个注解的元素, 创建新文件或发送验证信息.
一个聚合处理器将几个源聚合成一个或多个输出文件或验证信息. Gradle插件总是在重新处理所有被注释的文件, 而且文件在重新编译时不会被改变.
这两个类别都有以下限制:
- 它们只能读取
CLASS和RUNTIME注释. - 它们使用Filer API生成新文件(Java代码).
KAPT优化得很好, 但是Stubs生成是一个不能跳过的阶段. 为了解决这个问题, 谷歌开发了KSP(Kotlin符号处理), 其中Kotlin被原生支持, 并被添加到Kotlin编译器(而不是Java编译器), 并获得其所有功能和Kotlin代码的AST.
KSP
KSP与Kotlin代码一起工作, 可以创建Kotlin源文件, 然而, 它也有处理Java代码的能力. 这取决于KSP调用的是Java还是Kotlin源文件:
- 用于Java源文件的Java注释处理器
- 用于Kotlin源文件的Kotlin符号处理器
这意味着为了同时支持Java和Kotlin源文件, 你有必要支持这两个处理器.
KotlinPoet对JavaPoet有一个互操作, 使之与两种处理器兼容.
使用KSP的主要好处是编译速度比KAPT快2倍.
KSP本身是作为一个编译器插件实现的. Kotlin编译器插件具有编译器的全部功能, 另一方面取决于对编译器的修改, 所以KSP有必要与编译器的修改保持兼容.
class BuilderProcessor() : SymbolProcessor {
// Called by KSP to process a round of compilation
override fun process(resolver: Resolver): List<KSAnnotated> { }
// Called by KSP to finalise the complation processing
override fun finish() {}
// Called by KSP to handle errors for a round of compilation
override fun onError() {}
}
// Provides a certain processor that depends on an environment
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(
environment: SymbolProcessorEnvironment,
): SymbolProcessor {
return BuilderProcessor()
}
}
BuilderProcessorProvider类实现了SymbolProcessorProvider接口来创建一个处理器, 它能够根据输入环境使用不同的处理器.
processor有一个解析器, 可以访问编译器的细节, 如Symbols.finish被KSP调用, 以最终完成编译的处理.onError被KSP调用, 用于处理一轮编译的错误.
如何为KSP处理器编写测试
我的建议是使用Kotlin Compile Testing库, 只需添加源文件和符号过程提供者就可以运行测试. 所有困难的事情都隐藏在引擎盖下. 这个工具也可以用于KAPT测试.
val compilation = KotlinCompilation().apply {
sources = listOf(), // Denotes source files to be processed
// Denotes KSP processor providers to be used
symbolProcessorProviders = listOf(
BuilderProcessorProvider()
)
}
// Calls to run compilation
return compilation.compile()
据我了解, 你只能从Kotlin编译器中获得编译结果和信息, 该库有一个变通方法, 能够在一次编译器调用中运行处理器和编译. 输出文件是编译输入源文件和新生成的源文件.
val compilation = KotlinCompilation().apply {
// NOTE: https://github.com/tschuchortdev/kotlin-compile-testing/issues/312
// Enable KSP compilation
kspWithCompilation = true
}
增量构建
支持聚合和隔离两种模式的类别. 重要的区别是Java注释处理器为整个处理器定义了一个类别, KSP为每个输出单独定义了一个类别.
聚合类别的输出取决于任何输入的变化(除了删除没有引用其他来源的文件)并触发重新处理.
隔离类别输出依赖于指定的来源, 只有这些来源的任何变化才会触发再处理.
总的来说, 如果一个输出可能依赖于新的或任何改变的源, 它就是聚合的. 否则, 该输出是隔离的.
KSP的增量处理是默认启用的. 要禁用增量处理, 需要在gradle.properties中设置ksp.incremental=false.
文档中的重要说明。
只有Kotlin和Java源的变化被跟踪. 默认情况下, 对classpath的改变(对于其他模块或库)会触发对源的全面重新处理. 要跟踪classpath的变化, 请设置
ksp.incremental.intermodule=true以启用实验性实现.
为什么KSP还没有准备好大规模应用?
- 为了支持Java和Kotlin源文件, 必须同时实现Java注释处理和KSP.
集成开发环境:目前, 集成开发环境对生成的代码一无所知.
下面的代码编译正确, 但IDE将正确的代码(使用红色)突出显示为未解决的引用. 你有一种感觉, 代码是不正确的, 试图找到任何错误.
IDE将正确的代码(使用红色)突出显示为未解决的参考.
带有测试的完整源代码, 你可以在这里找到.
这个资源库有两个使用KSP和KAPT处理器的例子, 你可以在这里比较性能, 密切接触这些技术.
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情