本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端
今年初 Android 發佈了 Kotlin Symbol Processing(KSP)的首個 Alpha 版,幾個月過去,KSP 已經更新到 Beta3 了, 目前 API 已經基本穩定,相信距離穩定版發佈也不會很遠了。node
很多人吐槽 Kotlin 的編譯速度,KAPT 即是拖慢編譯的元兇之一。android
不少庫都會使用註解簡化模板代碼,例如 Room、Dagger、Retrofit 等,Kotlin 代碼使用 KAPT 處理註解。 KAPT 本質上是基於 APT 工做的,APT 只能處理 Java 註解,所以須要先生成 APT 可解析的 stub (Java代碼),這拖慢了 Kotlin 的總體編譯速度。git
KSP 正是在這個背景下誕生的,它基於 Kotlin Compiler Plugin(簡稱KCP) 實現,不須要生成額外的 stub,編譯速度是 KAPT 的 2 倍以上github
Kotlin Compiler Plugin 在 kotlinc 過程當中提供 hook 時機,能夠再次期間解析 AST、修改字節碼產物等,Kotlin 的很多語法糖都是 KCP 實現的,例如 data class
、 @Parcelize
、kotlin-android-extension
等, 現在火爆的 Compose 其編譯期工做也是藉助 KCP 完成的。後端
理論上 KCP 的能力是 KAPT 的超集,能夠替代 KAPT 以提高編譯速度。可是 KCP 的開發成本過高,涉及 Gradle Plugin、Kotlin Plugin 等的使用,API 涉及一些編譯器知識的瞭解,通常開發者很難掌握。api
一個標準 KCP 的開發涉及如下諸多內容:markdown
KSP 簡化了上述流程,開發者無需瞭解編譯器工做原理,處理註解等成本像 KAPT 同樣低。app
KSP 顧名思義,在 Symbols 級別對 Kotlin 的 AST 進行處理,訪問類、類成員、函數、相關參數等類型的元素。能夠類比 PSI 中的 Kotlin ASTjvm
一個 Kotlin 源文件經 KSP 解析後的結果以下:
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<KSVariableParameter>
// 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: KSVariableParameter
KSEnumEntryDeclaration
// same as KSClassDeclaration
複製代碼
這是 KSP 中的 Kotlin AST 抽象。 相似的, APT/KAPT 中有對 Java 的 AST 抽象,其中能找到一些對應關係,好比 Java 使用 Element
描述包、類、方法或者變量等, KSP 中使用 Declaration
Java/APT | Kotlin/KSP | Description |
---|---|---|
PackageElement | KSFile | 表示一個包程序元素。提供對有關包及其成員的信息的訪問 |
ExecuteableElement | KSFunctionDeclaration | 表示某個類或接口的方法、構造方法或初始化程序(靜態或實例),包括註釋類型元素 |
TypeElement | KSClassDeclaration | 表示一個類或接口程序元素。提供對有關類型及其成員的信息的訪問。注意,枚舉類型是一種類,而註解類型是一種接口 |
VariableElement | KSVariableParameter / KSPropertyDeclaration | 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數 |
Declaration
之下還有 Type 信息 ,好比函數的參數、返回值類型等,在 APT 中使用 TypeMirror
承載類型信息 ,KSP 中詳細的能力由 KSType
實現。
KSP 的開發流程和 KAPT 相似:
須要注意 KSP 不能用來修改原代碼,只能用來生成新代碼
KSP 經過 SymbolProcessor
來具體執行。SymbolProcessor
須要經過一個 SymbolProcessorProvider
來建立。所以 SymbolProcessorProvider
就是 KSP 執行的入口
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) {
//使用 FindFunctionsVisitor 遍歷訪問 AST
resolver.getAllFiles().map { it.accept(visitor, Unit) }
}
inner class FindFunctionsVisitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
//訪問 Class 節點
classDeclaration.getDeclaredFunctions().map { it.accept(this, Unit) }
}
override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
// 訪問 function 節點
functions.add(function)
}
override fun visitFile(file: KSFile, data: Unit) {
//訪問 file
file.declarations.map { it.accept(this, Unit) }
}
}
...
}
複製代碼
舉幾個例子看一下 KSP 的 API 是如何工做的
fun KSClassDeclaration.getDeclaredFunctions(): List<KSFunctionDeclaration> {
return this.declarations.filterIsInstance<KSFunctionDeclaration>()
}
複製代碼
fun KSDeclaration.isLocal(): Boolean {
return this.parentDeclaration != null && this.parentDeclaration !is KSClassDeclaration
}
複製代碼
fun KSDeclaration.isVisibleFrom(other: KSDeclaration): Boolean {
return when {
// locals are limited to lexical scope
this.isLocal() -> this.parentDeclaration == other
// file visibility or member
this.isPrivate() -> {
this.parentDeclaration == other.parentDeclaration
|| this.parentDeclaration == other
|| (
this.parentDeclaration == null
&& other.parentDeclaration == null
&& this.containingFile == other.containingFile
)
}
this.isPublic() -> true
this.isInternal() && other.containingFile != null && this.containingFile != null -> true
else -> false
}
}
複製代碼
// Find out suppressed names in a file annotation:
// @file:kotlin.Suppress("Example1", "Example2")
fun KSFile.suppressedNames(): List<String> {
val ignoredNames = mutableListOf<String>()
annotations.forEach {
if (it.shortName.asString() == "Suppress" && it.annotationType.resolve()?.declaration?.qualifiedName?.asString() == "kotlin.Suppress") {
it.arguments.forEach {
(it.value as List<String>).forEach { ignoredNames.add(it) }
}
}
}
return ignoredNames
}
複製代碼
最後看一個相對完整的例子,用來替代APT的代碼生成
@IntSummable
data class Foo(
val bar: Int = 234,
val baz: Int = 123
)
複製代碼
咱們但願經過KSP處理@IntSummable
,生成如下代碼
public fun Foo.sumInts(): Int {
val sum = bar + baz
return sum
}
複製代碼
開發 KSP 須要添加依賴:
plugins {
kotlin("jvm") version "1.4.32"
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation(kotlin("stdlib"))
implementation("com.google.devtools.ksp:symbol-processing-api:1.5.10-1.0.0-beta01")
}
複製代碼
咱們須要一個入口的 Provider
來構建 Processor
import com.google.devtools.ksp.symbol.*
class IntSummableProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return IntSummableProcessor(
options = environment.options,
codeGenerator = environment.codeGenerator,
logger = environment.logger
)
}
}
複製代碼
經過 SymbolProcessorEnvironment
能夠爲 Processor 注入了 options
、CodeGenerator
、logger
等所需依賴
class IntSummableProcessor() : SymbolProcessor {
private lateinit var intType: KSType
override fun process(resolver: Resolver): List<KSAnnotated> {
intType = resolver.builtIns.intType
val symbols = resolver.getSymbolsWithAnnotation(IntSummable::class.qualifiedName!!).filterNot{ it.validate() }
symbols.filter { it is KSClassDeclaration && it.validate() }
.forEach { it.accept(IntSummableVisitor(), Unit) }
return symbols.toList()
}
}
複製代碼
builtIns.intType
獲取到 kotlin.Int
的 KSType
, 在後面須要使用。getSymbolsWithAnnotation
獲取註解爲 IntSummable
的 symbols 列表Visitor 的接口通常以下,D
和 R
表明 Visitor 的輸入和輸出,
interface KSVisitor<D, R> {
fun visitNode(node: KSNode, data: D): R
fun visitAnnotated(annotated: KSAnnotated, data: D): R
// etc.
}
複製代碼
咱們的需求沒有輸入輸出,因此實現KSVisitorVoid
便可,本質上是一個 KSVisitor<Unit, Unit>
:
inner class Visitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
val qualifiedName = classDeclaration.qualifiedName?.asString()
//1. 合法性檢查
if (!classDeclaration.isDataClass()) {
logger.error(
"@IntSummable cannot target non-data class $qualifiedName",
classDeclaration
)
return
}
if (qualifiedName == null) {
logger.error(
"@IntSummable must target classes with qualified names",
classDeclaration
)
return
}
//2. 解析Class信息
//...
//3. 代碼生成
//...
}
private fun KSClassDeclaration.isDataClass() = modifiers.contains(Modifier.DATA)
}
複製代碼
如上,咱們判斷這個Class是否是data class
、其類名是否合法
接下來須要獲取 Class 中的相關信息,用於咱們的代碼生成:
inner class IntSummableVisitor : KSVisitorVoid() {
private lateinit var className: String
private lateinit var packageName: String
private val summables: MutableList<String> = mutableListOf()
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
//1. 合法性檢查
//...
//2. 解析Class信息
val qualifiedName = classDeclaration.qualifiedName?.asString()
className = qualifiedName
packageName = classDeclaration.packageName.asString()
classDeclaration.getAllProperties()
.forEach {
it.accept(this, Unit)
}
if (summables.isEmpty()) {
return
}
//3. 代碼生成
//...
}
override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: Unit) {
if (property.type.resolve().isAssignableFrom(intType)) {
val name = property.simpleName.asString()
summables.add(name)
}
}
}
複製代碼
KSClassDeclaration
獲取了className
, packageName
,以及 Properties
並將其存入 summables
visitPropertyDeclaration
中確保 Property 必須是 Int 類型,這裏用到了前面提到的 intType
收集完 Class 信息後,着手代碼生成。 咱們引入 KotlinPoet
幫助咱們生成 Kotlin 代碼
dependencies {
implementation("com.squareup:kotlinpoet:1.8.0")
}
複製代碼
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
//1. 合法性檢查
//...
//2. 解析Class信息
//...
//3. 代碼生成
if (summables.isEmpty()) {
return
}
val fileSpec = FileSpec.builder(
packageName = packageName,
fileName = classDeclaration.simpleName.asString()
).apply {
addFunction(
FunSpec.builder("sumInts")
.receiver(ClassName.bestGuess(className))
.returns(Int::class)
.addStatement("val sum = ${summables.joinToString(" + ")}")
.addStatement("return sum")
.build()
)
}.build()
codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = false),
packageName = packageName,
fileName = classDeclaration.simpleName.asString()
).use { outputStream ->
outputStream.writer()
.use {
fileSpec.writeTo(it)
}
}
}
複製代碼
FunSpec
生成 function 代碼CodeGenerator
用來建立文件,並寫入生成的FileSpec
代碼經過 IntSummable
的例子能夠看到 KSP 徹底能夠替代 APT/KAPT 進行註解處理,且性能更出色。
目前,已有很多使用 APT 的三方庫增長了對 KSP 的支持
Library | Status | Tracking issue for KSP |
---|---|---|
Room | Experimentally supported | |
Moshi | Experimentally supported | |
Kotshi | Experimentally supported | |
Lyricist | Experimentally supported | |
Auto Factory | Not yet supported | Link |
Dagger | Not yet supported | Link |
Hilt | Not yet supported | Link |
Glide | Not yet supported | Link |
DeeplinkDispatch | Not yet supported | Link |
將 KAPT 替換爲 KSP 也很是簡單,以 Moshi 爲例
固然,也能夠在項目中同時使用 KAPT 和 KSP ,他們互不影響。KSP 取代 KAPT 的趨勢愈來愈明顯,果你的項目也處理註解的需求,不妨試試 KSP ?