网易首页 > 网易号 > 正文 申请入驻

Kotlin Symbol Processing使用与原理剖析

0
分享至

一、KSP简介

Kotlin Symbol Processing (KSP) 是一个可用于开发轻量级编译器插件的API,与Kotlin Annotation Processing Tool(KAPT)相似,但是却可以更高效地处理注解,并提供更好的性能,使用 KSP 的注释处理器的运行速度最多可达两倍,而且可以支持多平台。

主要作用是为了让我们更轻松的编写代码,KSP广泛应用于元编程、自动化代码分析与代码生成,同时进行了性能优化。当基于KSP的插件处理处理源程序时,处理器可以访问类、类成员、函数和关联参数等结构。

二、为什么选择KSP

更快!更好的适配kotlin开发者!多平台的兼容性!

元编程是一种编程技术,编写出来的程序能够将其他程序作为数据来处理,他能够在编译时处理源码、中间代码,可以读取类、函数来执行某种逻辑,甚至可以在运行时读取程序自身的信息以及修改其结构。

常见的元编程技术手段有

  • Kotlin反射/Java反射

  • Kotlin注解处理器(Kotlin Annotation Processor Tool,KAPT)

  • Kotlin符号处理器(Kotlin Symbol Processing,KSP)

  • Kotlin 编译器插件(Kotlin Compiler Plugin,KCP)

对于反射,就不再详细解释了。随着kotlin的应用广泛化,为了适配JavaAPT,KAPT才应运而生,但是因为KAPT需要先转换成Java Stubs,同时在运行未经修改的Java注解处理器时候,KAPT需要将Kotlin代码编译为Java stubs,以保留Java注释处理器关注的信息,他需要解析Kotlin程序中的所有符号,Stub 生成的成本大约占1/3的kotlinc分析处理时间、且拥有跟kotlinc代码一样的生成顺序,所以对于许多注释处理器来说,这比处理器本身花费的时间要长得多。

所以说KAPT和JavaAPT实际上就是就是同一类产物, KAPT 的本质还是基于 Java 注解处理器实现的一个Kotlin 编译器插件。

例如,Glide 使用预定义的注解查看数量非常有限的类,并且其代码生成速度相当快。几乎所有的构建开销都存在于JavaStub生成阶段。如果切换到 KSP的话,就可以编译器上花费的时间减少25%。

通过下图就能看出KSP的优势

KCP是在 kotlinc 过程中提供 Hook 时机,可以在期间解析 AST、修改字节码产物等,理论上来说, KCP 的能力是 KAPT 的超集,完全可以替代 KAPT 以提升编译速度。但是 KCP 的开发成本太高,KSP 简化了KCP的整个流程,所以在项目里,我们完全可以先用KSP即可。

通过三方统计的对比图就可以看出来KSP所具有的优势:

通过引用网络上的相关测试数据(截取自叶楠-2023Kotlin中文开发者大会)可以看到编译时长的改进

三、KSP能得到什么

下面是KSP当中,经过解析后所看到的文件的格式,它包含了我们所需的各类常见内容:类、函数、属性等

有了这些想生成文件,拿到属性值就轻而易举了。

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

除此之外,我们可以从下图清晰看到整个KSP的导图

四、kapt迁移到ksp

对于大部分的项目,我们可以将kapt迁移到ksp。在大多数情况下,迁移只需要更改项目的build配置

迁移步骤

1、检查使用的库是否支持ksp(以下是最新官方提供的支持ksp的库)

很遗憾,Arouter并不支持

可以尝试迁移到货拉拉开源项目TheRouter,已经完美支持ksp

Hll_TheRouter

Library Status Room Officially supported Moshi Officially supported RxHttp Officially supported Kotshi Officially supported Lyricist Officially supported Lich SavedState Officially supported gRPC Dekorator Officially supported EasyAdapter Officially supported Koin Annotations Officially supported Glide Officially supported Micronaut Officially supported Epoxy Officially supported Paris Officially supported Auto Dagger Officially supported SealedX Officially supported DeeplinkDispatch Supported via airbnb/DeepLinkDispatch#323 Dagger Alpha Motif Alpha Hilt In progress Auto Factory Not yet supported
2、将KSP插件添加进项目中

注意要选择与项目的 Kotlin 版本一致的 KSP 版本,(这里demo使用kotlin版本是1.8.0,不同版本kotlin可能导致引入写法的区别)

可以在这里https://github.com/google/ksp/releases找到对应的KSP版本

//build.gradle.kts
plugins {
id( "org.jetbrains.kotlin.android" ) version "1.8.10" apply false
 id( "com.google.devtools.ksp" ) version "1.8.10-1.0.9" apply false
}

然后在对应的模块下添加

plugins {
 id( "com.google.devtools.ksp" )
}
dependencies {
 implementation( "com.google.devtools.ksp:symbol-processing-api:1.8.10-1.0.9" )
}
3、迁移支持KSP的项目依赖(举个栗子)

dependencies {
 kapt("com.google.dagger:dagger:3.5.0")
 ksp("com.google.dagger:dagger:3.5.0")
}
4、移除项目kapt相关等等

plugins {
 id("org.jetbrains.kotlin.kapt")
}
kapt {
 correctErrorTypes = true
 useBuildCache = true
}
5、可能遇到的问题

众所周知,在老的项目里,迁移过程大概率都不是一帆风顺的

部分问题:

1、在迁移过程中,首先我们的gradle需要支持gradle7.0+,这是一个大工程,如果我们要升级到最新的版本的话,还要同步升级java版本。如果你的项目里引入了热修复之类的科技,大概率也会有使用姿势的变更,这些问题在相关的github issue上一搜就是一堆(笔者祝你好运)

2、gradle的升级会面临一些插件的不兼容,比如笔者就曾遇到过项目里因为gradle tools的升级导致原本的方法失效,需要找到替代方案。比如asm的相关类:

org.objectweb.asm.tree.MethodNode org.objectweb.asm.tree.AnnotationNode org.objectweb.asm.tree.ClassNode

都已然失效。

3、另外gralde的升级,也少不了对Transform的升级改造,原本的类失效,以及未来可能不被兼容,都需要我们进行Transform->AsmClassVisitoFactory的迁移。据官方提供,AsmClassVisitoFactory会带来约18%的性能提升,同时可以减少约5倍代码,这是一个N全齐美的事情。

4、最后就是项目的三方架构,我们要统计好已支持ksp的库,并且升级到合适的版本,如果遇到不支持的,比如Arouter,欢迎使用Hll_TheRouter

当然了,之后我们还可以开启kotlin的增量编译,KSP支持增量编译,且比KAPT的增量编译更具有可配置性,下文我们会简单介绍一下ksp的增量编译,还有一个三方的适配工作,这些工作都做完后,相信我们的项目编译速度会有很大的提升。

五、使用

我们从以获取一个被注解的类的信息开始

1、创建一个ksp的module,存放我们测试ksp的代码

在module的build.gradle.kts下添加依赖,同时添加kotlinpoet,以便我们对文件进行编写

在ksp中,生成代码的方式是通过CodeGenerator 创建文件流后,进行字符串拼接,跟jsp一样都很繁琐,易出错。KotlinPoet是JavaPoet的Kotlin版本,通过生成代码,我们不用编写样板文件,同时还可以保留元数据的单一事实来源,使我们的开发更为简便,易读。

plugins {
id( "java-library" )
 id( "org.jetbrains.kotlin.jvm" )
 id( "com.google.devtools.ksp" )
}

dependencies {
 implementation("com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.16")
 implementation( "com.squareup:kotlinpoet:1.16.0" )
 implementation( "com.squareup:kotlinpoet-ksp:1.16.0" )
}

在项目的build.gradle.kts添加

plugins {
id( "org.jetbrains.kotlin.android" ) version "1.9.22" apply false
id( "com.google.devtools.ksp" ) version "1.9.22-1.0.16" apply false
}
2、实现 SymbolProcessorProvider

class DemoProcessorProvider: SymbolProcessorProvider {
 override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
 return DemoSymbolProcessor(environment)
 }
}

class DemoSymbolProcessor(
 private val environment: SymbolProcessorEnvironment
):SymbolProcessor {
 override fun process(resolver: Resolver): List { 
 //TODO 在这里处理我们的工作
 }
 }

在以下目录添加注册信息,是不是很熟悉的感觉,这跟我们使用transform编写插件的一种方式是一致的,都是用spi服务机制

在目录下将我们的provider进行注册即可

com.example.ksptest.DemoProcessorProvider

接下来我们就可以获取到注解信息了

3、定义注解和方法类

在我们的ksp module里自定义一个适用于function的注解

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class TestFunAnno (
// val ids: Int = 0
)

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class TestClassAnno (

)

class DemoSymbolProcessor( private val environment: SymbolProcessorEnvironment ) : SymbolProcessor { //TODO }
Resolver:提供编译器和符号相关的各种操作 getSymbolsWithAnnotation:找到被注解标记的符号(KSFile,KSPropertyDeclaration,KSClassDeclaration,KSFunctionDeclaration等,在我们的第三部分简介已经详细的列出) 通过filter,筛选过滤得到我们想要的参数。
4、在使用的module内引入 ksp module以及ksp plugin

plugins {
 id( "com.google.devtools.ksp" )
}
//写入ksp编译的生成目录
kotlin {
sourceSets.main {
kotlin.srcDir( "build/generated/ksp/test/kotlin" )
 }
}
//引入依赖及module
dependencies {
 implementation(project(mapOf( "path" to ":kspTest" )))
 ksp(project( ":kspTest" ))
}

添加测试类

@TestClassAnno
class MyClass {
 @TestFunAnno
 fun testFun(){

 }

 @TestFunAnno
 fun testFun2(){

 }
}
5、测试获取注解信息

class DemoSymbolProcessor( private val environment: SymbolProcessorEnvironment ) : SymbolProcessor { override fun process(resolver: Resolver): List { resolver.getSymbolsWithAnnotation(TestClassAnno::class.qualifiedName!!) .asSequence() .filterIsInstance () .forEach { it -> environment.logger.warn("DemoSymbolProcessor:" + it.toString()) } return emptyList() }

当我们筛选注解TestClassAnno 使用 KSClassDeclaration 时,打印的日志是

w: [ksp] DemoSymbolProcessor:MainActivity
w: [ksp] DemoSymbolProcessor:MyClass

当我们筛选注解TestFunAnno 使用 KSFunctionDeclaration 时,打印的日志是

w: [ksp] DemoSymbolProcessor:testFun
w: [ksp] DemoSymbolProcessor:testFun2
6、创造新的文件

基于上文的TestClassAnno, 我们利用原有信息创造一个被注解的类的副本

这里就需要用到kotlinpoet来协助生成文件,文档可参考:https://github.com/square/kotlinpoet

我们可以利用TypeSpec(创造文件)、PropertySpec(创造属性)、AnnotationSpec(创造注解)、FuncSpec(创造方法)、ParameterSpec(创造函数入参)等,来创造函数

override fun process(resolver: Resolver): List { 
 val list = resolver.getSymbolsWithAnnotation(TestClassAnno::class.qualifiedName!!)
 .asSequence()
 .filterIsInstance () 
 .forEach { it ->
environment.logger.warn( "DemoSymbolProcessor:" + it.toString())
 generate(environment, it)
 }

 return emptyList()
}

fun generate(environment: SymbolProcessorEnvironment, ksClassDeclaration: KSClassDeclaration) {
 //创建类文件
 val file = FileSpec.builder( "com.test.ksp.demo" , "My" +ksClassDeclaration.toString())
 //创建属性
 val proper = PropertySpec.builder( "properties" ,String::class,KModifier.PUBLIC) .initializer( "%S" , "properties" ) .addModifiers(KModifier.PUBLIC) .build()
 //创建函数定义
 val fun2 = FunSpec.builder( "fun2" )
 .build()
 //创建类的定义
 val classes = TypeSpec.classBuilder( "My" +ksClassDeclaration.toString())
 .addModifiers(KModifier.PUBLIC)
 .addFunction(fun2)
 .addProperty(proper)
 .build()

 //将所有创造的类信息塞到新创建的文件里
 file.addType(classes)
 .build()
 .writeTo(environment.codeGenerator,Dependencies(true))
 }

注意在这个demo里面,我们传了一个emptyList,简单的demo就不做过滤了。

正常情况下,process的返回值代表的意思是一个列表(处理器无法处理的延迟符号列表。仅应返回本轮无法处理的符号。已编译代码(库)中的符号始终有效,如果在延迟列表中返回,则会被忽略)

因为我们的符号可能并不都是合法的,要将未通过筛选的符号过滤一下并传进来,保证代码的正常运行。

val symbols = resolver.getSymbolsWithAnnotation(TestClassAnno::class.qualifiedName!!) .filterIsInstance () val ret = mutableListOf () symbols.toList().forEach { if (!it.validate()) 
 //收集未通过验证的符号 ret.add(it) else{ generate(environment, it) } } return ret

interface SymbolProcessor {
 /**
* Called by Kotlin Symbol Processing to run the processing task.
*
* @param resolver provides [SymbolProcessor] with access to compiler details such as Symbols.
* @return A list of deferred symbols that the processor can't process. Only symbols that can't be processed at this round should be returned. Symbols in compiled code (libraries) are always valid and are ignored if returned in the deferral list.
*/
 fun process(resolver: Resolver): List 

 /**
* Called by Kotlin Symbol Processing to finalize the processing of a compilation.
*/
 fun finish() {}

 /**
* Called by Kotlin Symbol Processing to handle errors after a round of processing.
*/
 fun onError() {}
}

值得注意的是,我们之前引入kotlinpoet同时也引入了kotlinpoet-ksp

implementation( "com.squareup:kotlinpoet:1.16.0" )
implementation( "com.squareup:kotlinpoet-ksp:1.16.0" )

FileType.writeTo具有不同的方法

这是kotlinpoet里面的:

这是kotlinpoet-ksp里面的:

所以要import:

import com.squareup.kotlinpoet.ksp.writeTo

最终可以在build文件目录下看到生成的文件

利用编译期间对注解的跟踪文件,我们可以想到butterknife,dagger,Arouter等api的基本实现逻辑,同时我们自己也可以利用ksp生成重复代码、模版代码、成产语法糖。。

六、增量编译

前文我们介绍过,ksp支持增量编译,增量处理是一种尽可能避免对源文件进行重新编译的处理技术,默认情况下,增量处理当前处于启用状态。要禁用它,可以设置 Gradle 属性ksp.incremental=false。要启用根据依赖项和输出转储脏集的日志,可以使用 ksp.incremental.log=true。可以在具有.log文件扩展名的build输出目录中找到这些日志文件。

目前AS的每次编译会删除所有旧生成的代码,并添加新的代码,KSP为了减少工作量会使用增量编译的方式只更新有关联更改的文件,我们在代码中通过设置Dependencies(aggregating = true, Files),可以设置是开启aggregating还是isolating模式,true代表不开启增量编译,默认开启。

file.addType(classes)
 .build()
 .writeTo(environment.codeGenerator,Dependencies(aggregating = true))
1、 KAPT的增量编译设置

Gradle增量注解处理器可以在resources/META-INF/gradle/incremental.annotation.processors下进行声明是属于哪一种,声明的语法如下:

注解处理器的全限定名,类别: org.gradle.IsoLationProcessor,isolating org.gradle.AggregatProcessor,aggregating

然后通过方法createResource设置进去

class Filer{
 FileObject createResource(JavaFileManager.Location location,
 CharSequence pkg,
 CharSequence relativeName,
 Element... originatingElements)
}
2、 KSP的增量编译设置

通过Dependencies和writeTo

class Dependencies private constructor(
 val isAllSources: Boolean,
 val aggregating: Boolean,
 val originatingFiles: List 
) {

 /**
* Create a [Dependencies] to associate with an output.
*
* @param aggregating 只要有改动存在,就invalidate重构.
*/
 constructor(aggregating: Boolean, vararg sources: KSFile) : this(false, aggregating, sources.toList())

 companion object {
 /**
* A short-hand to all source files.
*
* Associating an output to [ALL_SOURCES] essentially disables incremental processing, as the tiniest change will clobber all files.
* This should not be used in processors which care about processing speed.
*/
 val ALL_FILES = Dependencies(true, true, emptyList())
 }
}

public fun FileSpec.writeTo(
 codeGenerator: CodeGenerator,
 dependencies: Dependencies,
) {
 val file = codeGenerator.createNewFile(dependencies, packageName, name)
 // Don't use writeTo(file) because that tries to handle directories under the hood
OutputStreamWriter(file, StandardCharsets.UTF_8)
 .use(::writeTo)
}

writeTo的好处显而易见,对于每一个单独的生成文件都可以区分,而kapt则只能全部开启或全部不开启。

七、Java开发者参看

KSP很友好的考虑到了以前的java开发者,提供了Java annotation processing to KSP的文档参考,具体可以参看文档进行处理。下图部分截图展示

八、最后

给自己的项目引入ksp看起来是一项简单的工作,但是众所周知,没有简单的工作,只有复杂的项目,引入ksp改造自己的项目,任重道远,动起手来,为你的编译时长加把劲!!!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
疯狂27-0大逆转!公牛拒被雄鹿横扫终结11连败 吉迪20+14+10

疯狂27-0大逆转!公牛拒被雄鹿横扫终结11连败 吉迪20+14+10

醉卧浮生
2026-03-02 06:50:10
伊朗不太可能参与2026世界杯,如果弃战会由哪队补上?

伊朗不太可能参与2026世界杯,如果弃战会由哪队补上?

体育妞世界
2026-03-01 17:31:30
又挂了!伊朗防长和革命卫队总司令双遭斩首,指挥系统濒临瘫痪

又挂了!伊朗防长和革命卫队总司令双遭斩首,指挥系统濒临瘫痪

老马拉车莫少装
2026-03-01 00:01:08
约吗?挑战球技的那种

约吗?挑战球技的那种

飛娱日记
2026-03-02 08:34:32
轰然倒塌!自嗨锅母公司破产,从75亿估值到清零,只用了4年

轰然倒塌!自嗨锅母公司破产,从75亿估值到清零,只用了4年

流苏晚晴
2026-03-01 16:48:55
10条惊人的父子定律:当爸越“不正经”,养出的孩子越优秀

10条惊人的父子定律:当爸越“不正经”,养出的孩子越优秀

户外阿毽
2026-03-01 01:34:43
34岁评上副教授,直接躺平15年!山东一教师自曝生活状态,引争议

34岁评上副教授,直接躺平15年!山东一教师自曝生活状态,引争议

火山詩话
2026-02-27 09:09:49
拨乱反正!卡里克激活曼联,阿莫林“至暗时刻”已成过往

拨乱反正!卡里克激活曼联,阿莫林“至暗时刻”已成过往

乐道足球
2026-03-02 10:05:59
美国已成为第二个苏联?三大危机已浮出水面,它的下场比苏联更惨

美国已成为第二个苏联?三大危机已浮出水面,它的下场比苏联更惨

梦想的现实
2026-02-18 22:18:34
杨瀚森0分1板1助遭隔扣!老鹰4连胜擒开拓者 库明加20+7克林根两

杨瀚森0分1板1助遭隔扣!老鹰4连胜擒开拓者 库明加20+7克林根两

越岭寻踪
2026-03-02 09:34:14
高人预测:上海未来十年,这三大片区将迎来大发展!

高人预测:上海未来十年,这三大片区将迎来大发展!

瓜哥的动物日记
2026-03-02 01:03:16
史上最乱伦成语“上蒸下报”

史上最乱伦成语“上蒸下报”

华人星光
2026-02-21 11:24:05
刚爽完9天春节长假?别高兴太早!2027年春节可能没这么舒服了

刚爽完9天春节长假?别高兴太早!2027年春节可能没这么舒服了

王姐懒人家常菜
2026-03-02 00:46:38
同学拖家带口来旅游,5天花了5万,临走时我拦住她:把账结一下

同学拖家带口来旅游,5天花了5万,临走时我拦住她:把账结一下

奶茶麦子
2026-02-26 18:35:09
轻松应对多条战线:以色列趁机对黎巴嫩境内的目标发起猛烈攻势

轻松应对多条战线:以色列趁机对黎巴嫩境内的目标发起猛烈攻势

一种观点
2026-03-02 09:20:13
辅导员,全部入编

辅导员,全部入编

山东教育
2026-03-01 17:59:48
中国男篮100-93中国台北 球员评价:5人优秀,2人及格,5人低迷

中国男篮100-93中国台北 球员评价:5人优秀,2人及格,5人低迷

篮球资讯达人
2026-03-01 18:00:37
记者:上海和古德温保持联系,后者正在卡塔尔等待航班恢复

记者:上海和古德温保持联系,后者正在卡塔尔等待航班恢复

懂球帝
2026-03-01 23:40:44
又贵又臭!没你4战全胜,有你2战全输....

又贵又臭!没你4战全胜,有你2战全输....

柚子说球
2026-03-01 20:48:58
TVB视后宣萱的顶级炫富,不是豪车名表,而是她车后座的102岁保姆

TVB视后宣萱的顶级炫富,不是豪车名表,而是她车后座的102岁保姆

西楼知趣杂谈
2026-02-28 21:24:36
2026-03-02 10:40:49
Android群英传
Android群英传
Android群英传
455文章数 921关注度
往期回顾 全部

科技要闻

荣耀发布机器人手机、折叠屏、人形机器人

头条要闻

牛弹琴:伊朗之战比俄乌之战更生猛 给世界5个深刻教训

头条要闻

牛弹琴:伊朗之战比俄乌之战更生猛 给世界5个深刻教训

体育要闻

卡里克主场5连胜!队史第2人通过最大考验

娱乐要闻

美伊以冲突爆发,多位明星被困中东

财经要闻

中东局势影响如何?十大券商策略来了

汽车要闻

小米发布超跑! 游戏中对标布加迪法拉利

态度原创

家居
房产
数码
旅游
公开课

家居要闻

万物互联 享科技福祉

房产要闻

滨江九小也来了!集齐海侨北+哈罗、寰岛...江东教育要炸了!

数码要闻

内存成本前所未有:入门级PC将完全消失!不涨价根本不行

旅游要闻

2月大事件集锦 | 一文读遍旅游行业“新鲜事”!

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版