没想到吧,这世上除了元宇宙,还有元编程,如果没有接触过,可能会有点懵,不过没关系,简单的说就是用代码来生成代码。实现元编程的传统常见手段主要是使用 APT
注解处理器 + JavaPoet
组合拳,如果你是作为一名Android 开发者,一定在曾经或者现在使用过很多知名的开源库,比如ButterKnife
、ARouter
等,这些都是基于 注解处理器 + JavaPoet
的方式实现的元编程,是的,虽然元编程这个词很高大上,但是可能你已经默默的使用了很多年了。
元编程就是以源代码作为输入数据的程序,比如编译器、链接器、解释器、调试工具和程序分析工具等等,它们可以在编译时分析源码,对源码进行处理或修改,或者产生中间代码。当然主要的目的还是为了生成代码。
Reflection | KAPT | KSP | KCP | |
---|---|---|---|---|
运行时 | 慢 | - | - | - |
编译时 | - | 解析metadata | 基于 Kotlin AST | 基于 Kotlin AST |
复杂度 | 较低 | 中 | 中 | 较高 |
主要场景 | 提供动态能力 | 生成源码 | 生成源码 | 生成、修改IR |
现状 | 稳定 | 稳定 | 稳定 | 实验 |
多平台 | JVM + JS | JVM | 全部 | 全部 |
在进行Android应用开发时,不少人吐槽 Kotlin 的编译速度慢,而 KAPT 便是拖慢编译的元凶之一。我们知道,Android的很多库都会使用注解简化模板代码,例如 Room、Dagger、Retrofit 等,而默认情况下Kotlin 使用的是 KAPT 来处理注解的。KAPT没有专门的注解处理器,需要借助APT实现的,因此需要先生成 APT 可解析的 stub (Java代码),这拖慢了 Kotlin 的整体编译速度。
所以 KAPT 的本质还是基于 Java 注解处理器实现的一个Kotlin 编译器插件。
KAPT 处理 Kotlin 源码存在的问题:
KCP是在 kotlinc
过程中提供 Hook 时机,可以在期间解析 AST、修改字节码产物等,Kotlin 的不少语法糖都是 KCP 实现的。例如, data class、 @Parcelize、kotlin-android-extension
等,如今火爆的 Jetpack Compose
也是借助 KCP 完成的。
理论上来说, KCP 的能力是 KAPT 的超集,完全可以替代 KAPT 以提升编译速度。但是 KCP 的开发成本太高,涉及 Gradle Plugin、Kotlin Plugin 等的使用,API 涉及一些编译器知识的了解,一般开发者很难掌握。
KSP 简化了KCP的整个流程,开发者无需了解编译器工作原理,处理注解等成本也变得像 KAPT 一样低。
KSP 的全称是 Kotlin Symbol Processing ,Kotlin符号处理器,由Google开发,它提供了一套API可以开发轻量级的编译器插件。KSP 官网:https://github.com/google/ksp
KSP本身也是一种KCP插件的实现。
KSP API 根据Kotlin语法在符号级对Kotlin程序结构进行建模。当基于KSP 的插件处理源代码时,可以访问类、类成员、函数和相关参数等结构,但是不能访问 if 块和 for 循环等。
从概念上讲,KSP类似于Kotlin反射中的KType
。该API允许处理器从类声明导航到具有特定类型参数的对应类型,反之亦然。还可以替换类型参数、指定方差、应用星型投影和标记类型的可空性。
另一种理解KSP的方式是将其视为Kotlin程序的预处理器框架。编译中的数据流可以按照以下步骤描述:
与成熟的编译器插件不同,KSP不能修改代码。因为改变语言语义的编译器插件有时会让人非常困惑。KSP是以只读的方式来处理源代码,从而避免这种情况。
KSP被设计为隐藏编译器更改,最大限度地减少使用它的处理器的维护工作。KSP被设计成不与JVM绑定,因此将来可以更容易地适应其他平台。
KCP相比于KSP的不足:
KSP通过定义良好的API隐藏大多数编译器更改,尽管编译器甚至Kotlin语言的重大更改可能仍然需要向API用户公开。KSP试图通过提供一个API来实现常见的用例,该API以功能换取简单性。它的功能是一个通用kotlinc插件的严格子集。例如,kotlinc可以检查表达式和语句,甚至可以修改代码,而KSP不能。
KSP的API看起来类似于kotlin.reflect
。它们之间的主要区别是KSP中的类型引用需要显式地解析。这是不共享接口的原因之一。
KAPT使大量的Java注释处理器可以为Kotlin程序开箱即用。与KAPT相比,KSP的主要优点是改进了构建性能(不依赖于JVM)、更习惯的Kotlin API以及理解Kotlin专用符号的能力。
在性能方面,相比于 KAPT,使用KSP生成代码性能要快2倍以上,因为它省掉了生成 Java Stubs 的耗时过程。
为了不加修改的直接运行 Java
注解处理器,kapt
将 Kotlin 代码编译为 Java
桩代码(stub
),其中保留了 Java
注解处理器关注的信息。为了创建这些桩代码, kapt
需要解析 Kotlin
程序中的所有符号。桩代码生成占据了 kotlinc
完整分析过程的大约 1/3
, kotlinc
的代码生成过程也是如此。 对于很多注解处理器,这个过程比处理器本身耗费的时间要长很多。比如, Glide
只会分析使用了预定义注解的非常少量的类,它的代码生成非常快速, 几乎所有的构建开销都发生在桩代码生成阶段,切换到 KSP 可以立即减少编译器消耗时间的 25%
。
与 kapt
不同, KSP
中的处理器不会以 Java
的方式看待输入程序。 API
对 Kotlin
来说更加自然,尤其是对于 Kotlin
专有的功能,比如顶层函数。由于 KSP
不会象 kapt
那样将处理代理给 javac
, 因此它不会依赖于 JVM
专有的行为,并且将来有可能用于其它平台。
虽然KSP
试图成为大多数常见用例的简单解决方案,但与其他插件解决方案相比,它做了一些权衡。KSP
目前存在以下几点限制:
Java
注解处理API。KSP
生成的代码无法感知,必须手动为项目配置生成路径。大多数处理器通过输入源代码的各种程序结构进行导航。在深入研究API的使用之前,让我们看看从 KSP 的视角来看Kotlin源文件是怎样的:
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
这里列出了一个Kotlin源文件中声明的常见内容如: 类、函数、属性等等。该结构也被称为AST(抽象语法树),类似的, APT/KAPT 则是对 Java AST 的抽象,我们可以找到一些对应关系,比如 Java 使用 Element 描述包、类、方法或者变量等, KSP 中使用 Declaration。
在 KSP API 的底层实现中, 主要的资源消耗是类型解析。因此类型引用被设计为由处理器明确解析的类型(也有少数例外情况)。当一个类型(Type
) (比如 KSFunctionDeclaration.returnType
或 KSAnnotation.annotationType
)被引用时,它永远是一个 KSTypeReference
类型,这是一个带有注解和修饰符的 KSReferenceElement
。
interface KSFunctionDeclaration : ... {
val returnType: KSTypeReference?
// ...
}
interface KSTypeReference : KSAnnotated, KSModifierListOwner {
val type: KSReferenceElement
}
一个 KSTypeReference
可以解析为一个 KSType
, 它引用到 Kotlin
类型系统中的一个类型。
一个KSTypeReference
拥有一个 KSReferenceElement
, 它是 Kotlin
程序结构的数据模型:也就是类型引用是如何编写的。它对应于 Kotlin
语法中的 type
元素。
一个 KSReferenceElement
可以是一个 KSClassifierReference
或 KSCallableReference
,其中包含很多不需要解析的有用信息。 比如 KSClassifierReference
拥有 referencedName
,而 KSCallableReference
拥有 receiverType
, functionArguments
, 和 returnType
。
如果需要一个 KSTypeReference
引用的原始声明, 通常可以通过将其解析为 KSType
, 并通过访问 KSType.declaration
得到。要从一个类型得到它的类声明, 代码如下:
val ksType: KSType = ksTypeReference.resolve()
val ksDeclaration: KSDeclaration = ksType.declaration
类型解析的代价很高,因此需要明确调用。通过解析得到的有些信息在 KSReferenceElement
中已经存在了。 比如, 通过 KSClassifierReference.referencedName
可以过滤掉很多不感兴趣的元素。你应该只有在需要从 KSDeclaration
或 KSType
得到具体信息的时候才进行类型解析。
指向一个函数类型的 KSTypeReference
在它的元素中已经有了大部分信息。尽管可以解析到 Function0
, Function1
等等的函数群, 但这些解析不会带来比 KSCallableReference
更多的任何信息。有一种情况需要解析函数类型引用,就是处理函数原型(Function Prototype
)的 identity
.
Java / APT | KSP 中的类似功能 | 注意事项 |
---|---|---|
AnnotationMirror | KSAnnotation | |
AnnotationValue | KSValueArguments | |
Element | KSDeclaration/KSDeclarationContainer | |
ExecutableElement | KSFunctionDeclaration | |
PackageElement | KSFile | KSP不会将package 建模为程序元素 |
ExecuteableElement | KSFunctionDeclaration | 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素 |
TypeElement | KSClassDeclaration | 一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口 |
VariableElement | KSVariableParameter / KSPropertyDeclaration | 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 |
Parameterizable | KSDeclaration | |
QualifiedNameable | KSDeclaration | |
TypeElement | KSClassDeclaration | |
TypeParameterElement | KSTypeParameter | |
VariableElement | KSValueParameter/KSPropertyDeclaration |
KSP 要求明确解析类型, 因此在解析之前, Java 中的有些功能只能通过 KSType
和对应的元素得到.
Java / APT | KSP 中的类似功能 | 注意事项 |
---|---|---|
ArrayType | KSBuiltIns.arrayType | |
DeclaredType | KSType / KSClassifierReference | |
ErrorType | KSType.isError | |
ExecutableType | KSType / KSCallableReference | |
IntersectionType | KSType / KSTypeParameter | |
NoType | KSType.isError | KSP 中没有这样的功能 |
NullType | KSP 中没有这样的功能 | |
PrimitiveType | KSBuiltIns | 与 Java 中的基本类型不完全相同 |
ReferenceType | KSTypeReference | |
TypeMirror | KSType | |
TypeVariable | KSTypeParameter | |
UnionType | 没有这样的功能 Kotlin 的 每个 catch 代码段只有 1 个类型. 即使对 Java 注解处理器来说, UnionType 也是不可访问的 |
|
WildcardType | KSType / KSTypeArgument |
Java / APT | KSP 中的类似功能 | 注意事项 |
---|---|---|
Name | KSName | |
ElementKind | ClassKind / FunctionKind | |
Modifier | Modifier | |
NestingKind | ClassKind / FunctionKind | |
AnnotationValueVisitor | ||
ElementVisitor | KSVisitor | |
AnnotatedConstruct | KSAnnotated | |
TypeVisitor | ||
TypeKind | KSBuiltIns | 有些可以在 builtin 中得到, 其他通过 KSClassDeclaration 得到 DeclaredType |
ElementFilter | Collection.filterIsInstance | |
ElementKindVisitor | KSVisitor | |
ElementScanner | KSTopDownVisitor | |
SimpleAnnotationValueVisitor | KSP 中不需要 | |
SimpleElementVisitor | KSVisitor | |
SimpleTypeVisitor | ||
TypeKindVisitor | ||
Types | Resolver / utils | 有些 utils 也被集成在符号接口中 |
Elements | Resolver / utils |
这部分介绍 KSP 怎样提供 Java 注解处理 API 的功能.
Java | KSP 中的同等功能 |
---|---|
getAnnotationType | ksAnnotation.annotationType |
getElementValues | ksAnnotation.arguments |
Java | KSP 中的同等功能 |
---|---|
getValue | ksValueArgument.value |
Java | KSP 中的同等功能 |
---|---|
asType | ksClassDeclaration.asType(…) |
getAnnotation | 未实现 |
getAnnotationMirrors | ksDeclaration.annotations |
getEnclosedElements | ksDeclarationContainer.declarations |
getEnclosingElements | ksDeclaration.parentDeclaration |
getKind | 通过 ClassKind 或 FunctionKind 进行类型检查和转换 |
getModifiers | ksDeclaration.modifiers |
getSimpleName | ksDeclaration.simpleName |
Java | KSP 中的同等功能 |
---|---|
getDefaultValue | 未实现 |
getParameters | ksFunctionDeclaration.parameters |
getReceiverType | ksFunctionDeclaration.parentDeclaration |
getReturnType | ksFunctionDeclaration.returnType |
getSimpleName | ksFunctionDeclaration.simpleName |
getThrownTypes | Kotlin 中不需要 |
getTypeParameters | ksFunctionDeclaration.typeParameters |
isDefault | 检查父类型是不是接口 |
isVarArgs | ksFunctionDeclaration.parameters.any { it.isVarArg } |
Java | KSP 中的同等功能 |
---|---|
getTypeParameters | ksFunctionDeclaration.typeParameters |
Java | KSP 中的同等功能 |
---|---|
getQualifiedName | ksDeclaration.qualifiedName |
Java | KSP 中的同等功能 |
---|---|
getEnclosedElements | ksClassDeclaration.declarations |
getEnclosingElement | ksClassDeclaration.parentDeclaration |
getInterfaces | // 不需要类型解析也应该能够实现 ksClassDeclaration.superTypes .map { it.resolve() } .filter { (it?.declaration as? KSClassDeclaration)?.classKind == ClassKind.INTERFACE } |
getNestingKind | Check KSClassDeclaration.parentDeclaration 和 inner 修饰符 |
getQualifiedName | ksClassDeclaration.qualifiedName |
getSimpleName | ksClassDeclaration.simpleName |
getSuperclass | // 不需要类型解析也应该能够实现 ksClassDeclaration.superTypes .map { it.resolve() } .filter { (it?.declaration as? KSClassDeclaration)?.classKind == ClassKind.CLASS } |
getTypeParameters | ksClassDeclaration.typeParameters |
Java | KSP 中的同等功能 |
---|---|
getBounds | ksTypeParameter.bounds |
getEnclosingElement | ksTypeParameter.parentDeclaration |
getGenericElement | ksTypeParameter.parentDeclaration |
Java | KSP 中的同等功能 |
---|---|
getConstantValue | 未实现 |
getEnclosingElement | ksValueParameter.parentDeclaration |
getSimpleName | ksValueParameter.simpleName |
Java | KSP 中的同等功能 |
---|---|
getComponentType | ksType.arguments.first() |
Java | KSP 中的同等功能 |
---|---|
asElement | ksType.declaration |
getEnclosingType | ksType.declaration.parentDeclaration |
getTypeArguments | ksType.arguments |
函数的 KSType
只是一个签名, 由 FunctionN
群表达.
Java | KSP 中的同等功能 |
---|---|
getParameterTypes | ksType.declaration.typeParameters, ksFunctionDeclaration.parameters.map { it.type } |
getReceiverType | ksFunctionDeclaration.parentDeclaration.asType(…) |
getReturnType | ksType.declaration.typeParameters.last() |
getThrownTypes | Kotlin 中不需要 |
getTypeVariables | ksFunctionDeclaration.typeParameters |
Java | KSP 中的同等功能 |
---|---|
getAllAnnotationMirrors | KSDeclarations.annotations |
getAllMembers | getAllFunctions, getAllProperties未实现 |
getBinaryName | 未决定, 参见 Java Specification |
getConstantExpression | 常数值, 而不是表达式 |
getDocComment | 未实现 |
getElementValuesWithDefaults | 未实现 |
getName | resolver.getKSNameFromString |
getPackageElement | 不支持包, 但可以取得包信息. KSP 中不能对包进行操作. |
getPackageOf | 不支持包 |
getTypeElement | Resolver.getClassDeclarationByName |
hides | 未实现 |
isDeprecated | KsDeclaration.annotations.any { it.annotationType.resolve()!!.declaration.qualifiedName!!.asString()== Deprecated::class.qualifiedName} |
overrides | KSFunctionDeclaration.overrides / KSPropertyDeclaration.overrides (各个类的成员函数) |
printElements | KSP 对大多数类有基本的 toString() 实现 |
Java | KSP 中的同等功能 |
---|---|
asElement | ksType.declaration |
asMemberOf | resolver.asMemberOf |
boxedClass | 不需要 |
capture | 未决定 |
contains | KSType.isAssignableFrom |
directSuperTypes | (ksType.declaration as KSClassDeclaration).superTypes |
erasure | ksType.starProjection() |
getArrayType | ksBuiltIns.arrayType.replace(…) |
getDeclaredType | ksClassDeclaration.asType |
getNoType | ksBuiltIns.nothingType / null |
getNullType | 根据上下文确定, 可能可以使用 KSType.markNullable |
getPrimitiveType | 不需要, 检查 KSBuiltins |
getWildcardType | 在需要 KSTypeArgument 的地方使用 Variance |
isAssignable | ksType.isAssignableFrom |
isSameType | ksType.equals |
isSubsignature | functionTypeA == functionTypeB / functionTypeA == functionTypeB.starProjection() |
isSubtype | ksType.isAssignableFrom |
unboxedType | 不需要 |
在 Android Studio 已有的kotlin项目中新建一个普通的 library 工程作为KSP处理模块(其他IDE配置请参考官网),在其build.gradle中添加如下配置:
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
java.sourceSets {
main {
java.srcDirs += "src/main/kotlin"
}
}
dependencies {
implementation 'com.google.devtools.ksp:symbol-processing-api:1.7.21-1.0.8'
}
ksp依赖库的版本需要根据项目使用的kotlin版本来决定,最新版本的Android Studio一般默认是在根目录下的build.gradle中可以找到kotlin版本配置。然后到KSP的发布页找到对应的KSP版本即可:https://github.com/google/ksp/releases
在 library module 中需要新建一个 SymbolProcessorProvider
的实现类作为KSP的入口,SymbolProcessorProvider
接口的代码如下:
interface SymbolProcessorProvider {
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}
可以看到它只有一个 create
方法,该方法需要返回一个实现 SymbolProcessor
接口的对象,而 create
方法的入参SymbolProcessorEnvironment
主要就是用来给创建 SymbolProcessor
对象用的,通过environment
参数可以获取到 KSP 运行时的相关依赖,我们只需将这些依赖注入到自定义的 Processor 对象即可。
class ProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return Processor(environment.codeGenerator, environment.logger,environment.options)
}
}
这里创建Processor使用了SymbolProcessorEnvironment
的三个字段:
codeGenerator
:可以用来生成代码文件logger
:可以用来输出日志options
:可以用来接受命令行或Gradle插件中的配置参数一般来说,codeGenerator
参数是一定要的,因为你总要生成代码吧,而其他的参数可以根据自己的需要选择。通过查看SymbolProcessorEnvironment
的源码可以知道全部可用的字段:
class SymbolProcessorEnvironment(
/**
* passed from command line, Gradle, etc.
*/
val options: Map<String, String>,
/**
* language version of compilation environment.
*/
val kotlinVersion: KotlinVersion,
/**
* creates managed files.
*/
val codeGenerator: CodeGenerator,
/**
* for logging to build output.
*/
val logger: KSPLogger,
/**
* Kotlin API version of compilation environment.
*/
val apiVersion: KotlinVersion,
/**
* Kotlin compiler version of compilation environment.
*/
val compilerVersion: KotlinVersion,
/**
* Information of target platforms
*
* There can be multiple platforms in a metadata compilation.
*/
val platforms: List<PlatformInfo>,
) {
...}
当我们创建好 SymbolProcessorProvider
对象后就可以先将其添加到src/main/resources/META-INF/services/
路径下的一个名为com.google.devtools.ksp.processing.SymbolProcessorProvider
的文件中:
在上面的文件中,输入ProcessorProvider对象的全类名,例如:
com.fly.ksp.processor.ProcessorProvider
SymbolProcessor
接口类就是KSP开发时唯一需要重点关注的类
interface SymbolProcessor {
fun process(resolver: Resolver): List<KSAnnotated> // 重点关注
fun finish() {
}
fun onError() {
}
}
它有三个方法,但唯一需要覆写的只有 process
这个方法,下面定义一个类来实现该接口:
class Processor(val codeGenerator: CodeGenerator, val logger: KSPLogger) : SymbolProcessor {
val functions = mutableListOf<String>()
val visitor = FindFunctionsVisitor()
override fun process(resolver: Resolver) {
resolver.getAllFiles().map {
it.accept(visitor, Unit) }
}
}
SymbolProcessor.process()
方法提供了一个 Resolver
, 来解析源文件的 symbols,而Resolver
使用访问者模式去遍历 AST,需要一个KSVisitor
参数。
下面代码定义了一个 FindFunctionsVisitor
给 Resolver
使用,在这个Visitor中负责找出当前 KSFile
中的 top-level
的 function
以及 Class
成员方法。
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) }
}
}
fun KSClassDeclaration.getDeclaredFunctions(): List<KSFunctionDeclaration> =
declarations.filterIsInstance<KSFunctionDeclaration>()
fun KSDeclaration.isLocal(): Boolean =
parentDeclaration != null && parentDeclaration !is KSClassDeclaration
fun KSTypeAlias.findActualType(): KSClassDeclaration {
val resolvedType = this.type.resolve().declaration
return if (resolvedType is KSTypeAlias) {
resolvedType.findActualType()
} else {
resolvedType as KSClassDeclaration
}
}
// @file:kotlin.Suppress("Example1", "Example2")
fun KSFile.suppressedNames(): List<String> {
val ignoredNames = mutableListOf<String>()
annotations.filter {
it.shortName.asString() == "Suppress" && it.annotationType.resolve()?.declaration?.qualifiedName?.asString() == "kotlin.Suppress"
}.forEach {
val argValues: List<String> = it.arguments.flatMap {
it.value }
ignoredNames.addAll(argValues)
}
return ignoredNames
}
现在有一个类,代码如下,假如我们现在想要为其生成建造者模式的代码
class AClass(private val a: Int, val b: String, val c: Double) {
val p = "$a, $b, $c"
fun foo() = p
}
尽管 kotlin 中支持默认参数值和命名参数,基本上可以取代建造者模式的使用了,但是假如你更喜欢建造者模式的使用方式,你仍然可以通过代码来编写它,问题是这样的代码有大量重复的样板代码需要编写,十分的消耗体力,那么此时使用KSP就可以为我们节省劳动力。
假如我们期望在生成建造者模式的代码之后使用方式如下:
@Builder
class AClass(private val a: Int, val b: String, val c: Double) {
val p = "$a, $b, $c"
fun foo() = p
}
fun main() {
val a = AClassBuilder()