一、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<KSAnnotation> (File annotations)
declarations: List<KSDeclaration>
KSClassDeclaration // class, interface, object
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
classKind: ClassKind
primaryConstructor: KSFunctionDeclaration
superTypes: List<KSTypeReference>
// contains inner classes, member functions, properties, etc.
declarations: List<KSDeclaration>
KSFunctionDeclaration // top level function
simpleName: KSName
qualifiedName: KSName
containingFile: String
typeParameters: KSTypeParameter
parentDeclaration: KSDeclaration
functionKind: FunctionKind
extensionReceiver: KSTypeReference?
returnType: KSTypeReference
parameters: List<KSValueParameter>
// contains local classes, local functions, local variables, etc.
declarations: List<KSDeclaration>
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<KSAnnotated> // 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<String>()
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 |