KSP准备好大规模应用了吗? - 2

652 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情

续接上文: KSP准备好大规模应用了吗? - 1

什么是KAPT?

KAPT(Kotlin注释处理工具)将处理分为两个步骤:

  • 从Kotlin源文件(包含处理所需的所有内容)生成存根Java类.
  • 合并所有的源文件, 包括存根, 将其传递给编译器.

简单地说, KAPT内部与Java源文件一起工作. 因此, 同样的注解处理器被用于.

Stub的生成增加了编译时间, 新生成的源代码必须用Java编写, 并兼容Kotlin类型.

增量构建

从Gradle插件4.7开始, 编译器也支持增量注解处理.

有2种类别:

  • 隔离类型
  • 聚合类型

此外, 还有dynamic模式, 处理器通过getSupportedOptions方法定义将使用哪个类别.

一个隔离式处理器在隔离中处理每个注解的元素, 创建新文件或发送验证信息.

一个聚合处理器将几个源聚合成一个或多个输出文件或验证信息. Gradle插件总是在重新处理所有被注释的文件, 而且文件在重新编译时不会被改变.

这两个类别都有以下限制:

  • 它们只能读取CLASSRUNTIME注释.
  • 它们使用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源文件, 你有必要支持这两个处理器.

KotlinPoetJavaPoet有一个互操作, 使之与两种处理器兼容.

使用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将正确的代码(使用红色)突出显示为未解决的引用. 你有一种感觉, 代码是不正确的, 试图找到任何错误.

1_330ppXseB1ND6ThyYYEFkg.webp

IDE将正确的代码(使用红色)突出显示为未解决的参考.

带有测试的完整源代码, 你可以在这里找到.

github.com/maluginp/ks…

这个资源库有两个使用KSP和KAPT处理器的例子, 你可以在这里比较性能, 密切接触这些技术.

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情