一、KSP
在进行Android应用开发时,不少人吐槽 Kotlin 的编译速度慢,而KAPT 便是拖慢编译的元凶之一。我们知道,Android的很多库都会使用注解简化模板代码,例如 Room、Dagger、Retrofit 等,而默认情况下Kotlin 使用的是 KAPT 来处理注解的。KAPT没有专门的注解处理器,需要借助APT实现的,因此需要先生成 APT 可解析的 stub (Java代码),这拖慢了 Kotlin 的整体编译速度。
KSP 正是在这个背景下诞生的,它基于 Kotlin Compiler Plugin(简称KCP) 实现,不需要生成额外的 stub,编译速度是 KAPT 的 2 倍以上。除了 大幅提高 Kotlin 开发者的构建速度,该工具还提供了对 Kotlin/Native 和 Kotlin/JS 的支持。
二、KSP 与 KCP
这里提到了Kotlin Compiler Plugin ,KCP是在 kotlinc 过程中提供 Hook 时机,可以在期间解析 AST、修改字节码产物等,Kotlin 的不少语法糖都是 KCP 实现的。例如, data class、 @Parcelize、kotlin-android-extension 等,如今火爆的 Jetpack Compose也是借助 KCP 完成的。
理论上来说, KCP 的能力是 KAPT 的超集,完全可以替代 KAPT 以提升编译速度。但是 KCP 的开发成本太高,涉及 Gradle Plugin、Kotlin Plugin 等的使用,API 涉及一些编译器知识的了解,一般开发者很难掌握。一个标准 KCP 架构如下所示。
- Plugin:Gradle 插件用来读取 Gradle 配置传递给 KCP(Kotlin Plugin);
- Subplugin:为 KCP 提供自定义 Kotlin Plugin 的 maven 库地址等配置信息;
- CommandLineProcessor:将参数转换为 Kotlin Plugin 可识别参数;
- ComponentRegistrar:注册 Extension 到 KCP 的不同流程中;
- Extension:实现自定义的Kotlin Plugin功能;
KSP 简化了KCP的整个流程,开发者无需了解编译器工作原理,处理注解等成本也变得像 KAPT 一样低。
三、KSP 与 KAPT
KSP 顾名思义,在 Symbols 级别对 Kotlin 的 AST 进行处理,访问类、类成员、函数、相关参数等类型的元素。可以类比 PSI 中的 Kotlin AST,结构如下图。
可以看到,一个 Kotlin 源文件经 KSP 解析后得到的 Kotlin AST如下所示。
KSFile
packageName: KSName
fileName: String
annotations: List (File annotations)
declarations: List
KSClassDeclaration // class, interface, object
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
classKind: ClassKind
primaryConstructor: KSFunctionDeclaration
superTypes: List
// contains inner classes, member functions, properties, etc.
declarations: List
KSFunctionDeclaration // top level function
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
functionKind: FunctionKind
extensionReceiver: KSTypeReference?
returnType: KSTypeReference
parameters: List
// contains local classes, local functions, local variables, etc.
declarations: List
KSPropertyDeclaration // global variable
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
extensionReceiver: KSTypeReference?
type: KSTypeReference
getter: KSPropertyGetter
returnType: KSTypeReference
setter: KSPropertySetter
parameter: KSValueParameter
类似的, APT/KAPT 则是对 Java AST 的抽象,我们可以找到一些对应关系,比如 Java 使用 Element 描述包、类、方法或者变量等, KSP 中使用 Declaration。
Java/APT | Kotlin/KSP | 描述 |
---|---|---|
PackageElement | KSFile | 一个包程序元素,提供对有关包及其成员的信息的访问 |
ExecuteableElement | KSFunctionDeclaration | 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素 |
TypeElement | KSClassDeclaration | 一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口 |
VariableElement | KSVariableParameter / KSPropertyDeclaration | 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 |
Declaration 之下还有 Type 信息 ,比如函数的参数、返回值类型等,在 APT 中使用 TypeMirror 承载类型信息 ,KSP 中详细的能力由 KSType 实现。
四、KSP 入口SymbolProcessorProvider
KSP的入口在SymbolProcessorProvider ,代码如下:
interface SymbolProcessorProvider {
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}
SymbolProcessorEnvironment 主要用于获取 KSP 运行时的依赖,注入到 Processor:
interface SymbolProcessor {
fun process(resolver: Resolver): List // Let's focus on this
fun finish() {}
fun onError() {}
}
process() 方法需要提供一个 Resolver , 解析 AST 上的 symbols,Resolver 使用访问者模式去遍历 AST。如下,Resolver 使用 FindFunctionsVisitor 找出当前 KSFile 中 top-level 的 function 以及 Class 成员方法。
class HelloFunctionFinderProcessor : SymbolProcessor() {
...
val functions = mutableListOf()
val visitor = FindFunctionsVisitor()
override fun process(resolver: Resolver) {
resolver.getAllFiles().map { it.accept(visitor, Unit) }
}
inner class FindFunctionsVisitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
classDeclaration.getDeclaredFunctions().map { it.accept(this, Unit) }
}
override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
functions.add(function)
}
override fun visitFile(file: KSFile, data: Unit) {
file.declarations.map { it.accept(this, Unit) }
}
}
...
class Provider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = ...
}
}
五、快速上手
5.1 创建processor
首先,创建一个空的gradle工程。
然后,在根项目中指定Kotlin插件的版本,以便在其他项目模块中使用,比如。
plugins {
kotlin("jvm") version "1.6.0" apply false
}
buildscript {
dependencies {
classpath(kotlin("gradle-plugin", version = "1.6.0"))
}
}
不过,为了统一项目中Kotlin的版本,可以在gradle.properties文件中进行统一的配置。
kotlin.code.style=official
kotlinVersion=1.6.0
kspVersion=1.6.0-1.0.2
接着,添加一个用于承载处理器的模块。并在模块的build.gradle.kts文件中添加如下脚步。
plugins {
kotlin("jvm")
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.6.0-1.0.2")
}
接着,我们需要实现com.google.devtools.ksp.processing.SymbolProcessor和com.google.devtools.ksp.processing.SymbolProcessorProvider。SymbolProcessorProvider的实现作为一个服务加载,以实例化实现的SymbolProcessor。使用时需要注意以下几点:
- 使用SymbolProcessorProvider.create()来创建一个SymbolProcessor。processor需要的依赖则可以通过SymbolProcessorProvider.create()提供的参数进行传递。
- 主要逻辑应该在SymbolProcessor.process()方法中执行。
- 使用resoler.getsymbolswithannotation()来获得我们想要处理的内容,前提是给出注释的完全限定名称,比如
com.example.annotation.Builder
。 - KSP的一个常见用例是实现一个定制的访问器,接口
com.google.devtools.ksp.symbol.KSVisitor
,用于操作符号。 - 有关SymbolProcessorProvider和SymbolProcessor接口的使用示例,请参见示例项目中的以下文件:
src/main/kotlin/BuilderProcessor.kt
和src/main/kotlin/TestProcessor.kt
。 - 编写自己的处理器之后,在
resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
中包含处理器提供商的完全限定名,将其注册到包中。
5.2 使用processor
5.2.1 使用Kotlin DSL
再创建一个模块,包含处理器需要处理的工作。然后,在build.gradle.kts文件中添加如下代码。
pluginManagement {
repositories {
gradlePluginPortal()
}
}
在新模块的build.gradle中,我们主要完成以下事情:
- 应用带有指定版本的com.google.devtools.ksp插件。
- 将ksp添加到依赖项列表中
比如:
plugins {
id("com.google.devtools.ksp") version kspVersion
kotlin("jvm") version kotlinVersion
}
运行./gradlew
命令进行构建,可以在build/generated/source/ksp
下找到生成的代码。下面是一个build.gradle.kts将KSP插件应用到workload的示例。
plugins {
id("com.google.devtools.ksp") version "1.6.0-1.0.2"
kotlin("jvm")
}
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(project(":test-processor"))
ksp(project(":test-processor"))
}
5.2.2 使用Groovy
在您的项目中构建。Gradle文件添加了一个包含KSP插件的插件块
plugins {
id "com.google.devtools.ksp" version "1.5.31-1.0.0"
}
然后,在dependencies添加如下依赖。
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation project(":test-processor")
ksp project(":test-processor")
}
SymbolProcessorEnvironment提供了processors选项,选项在gradle构建脚本中指定。
ksp {
arg("option1", "value1")
arg("option2", "value2")
...
}
5.3 使用IDE生成代码
默认情况下,IntelliJ或其他ide是不知道生成的代码的,因此对这些生成符号的引用将被标记为不可解析的。为了让IntelliJ能够对生成的代码进行操作,需要添加如下配置。
build/generated/ksp/main/kotlin/
build/generated/ksp/main/java/
当然,也可以是资源目录。
build/generated/ksp/main/resources/
在使用的时候,还需要在KSP processor模块中配置这些生成的目录。
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
}
sourceSets.test {
kotlin.srcDir("build/generated/ksp/test/kotlin")
}
}
如果在Gradle插件中使用IntelliJ IDEA和KSP,那么上面的代码片段会给出以下警告:
Execution optimizations have been disabled for task ':publishPluginJar' to ensure correctness due to the following reasons:
对于这种警告,我们可以在模块中添加下面的代码。
plugins {
// …
idea
}
// …
idea {
module {
// Not using += due to https://github.com/gradle/gradle/issues/8749
sourceDirs = sourceDirs + file("build/generated/ksp/main/kotlin") // or tasks["kspKotlin"].destination
testSourceDirs = testSourceDirs + file("build/generated/ksp/test/kotlin")
generatedSourceDirs = generatedSourceDirs + file("build/generated/ksp/main/kotlin") + file("build/generated/ksp/test/kotlin")
}
}
目前,已有不少使用 APT 的三方库增加了对 KSP 的支持,如下。
Library | Status | Tracking issue for KSP |
---|---|---|
Room | Experimentally supported | |
Moshi | Officially supported | |
RxHttp | Officially supported | |
Kotshi | Officially supported | |
Lyricist | Officially supported | |
Lich SavedState | Officially supported | |
gRPC Dekorator | Officially supported | |
Auto Factory | Not yet supported | Link |
Dagger | Not yet supported | Link |
Hilt | Not yet supported | Link |
Glide | Not yet supported | Link |
DeeplinkDispatch | Supported via airbnb/DeepLinkDispatch#323 |