theme: smartblue
前言
大家退后,今天我要开始表演一下装逼的艺术。这次我们尝试性的使用谷歌前一阵子公布的ksp(Kotlin Symbol Processing),一款专门拿来给Kotlin
项目提升注解生成速度的。
在ksp
出来以前,对于这种注解解释器,我们使用的都是java所提供的AbstractProcessor
,我在以前的文章介绍过关于AbstractProcessor相关的内容。这次我们就来啃一啃谷歌的新东西,毕竟只要我E的够快,谁的问好都追不上我。
Kotlin Symbol Processing (KSP) is an API that you can use to develop lightweight compiler plugins. KSP provides a simplified compiler plugin API that leverages the power of Kotlin while keeping the learning curve at a minimum. Compared to KAPT, annotation processors that use KSP can run up to 2x faster.
官方对于ksp的介绍就是,这是一个轻量级替换kapt的一个方案,优点就是速度更快,参数更少更简单一点。但是天下武功唯快不破啊,编译速度提升这种事情吧,毕竟都很难。
同时ksp
相比于kapt
接入方式也更清凉,还有就是它本身也支持增量编译等。有兴趣的老哥可以仔细阅读下谷歌对于ksp
的介绍,下面是传送门。
开始项目介绍吧
这次我们从头开始撸一个实现了ksp的compiler,至于代码就还是放在我们的路由组件内,毕竟如果从头写一个带注解的也有点不知所措,还是在历史的Demo上开发吧,下面是地址哦。
Router地址地址如下
由于官方文档中推荐我们使用gradle.kts
,所以这次对项目整体进行了升级。这部分后续会更新另外一个文章,凡事还是逃不开真香定律的,这东西相比于Groovy
对开发也更友善一点,毕竟kts还是强类型语言。
第一步
第一步比较简单,主要是对项目的根目录的Gradle进行配置上的修改,让我们可以顺利的获取到ksp
的引用。虽然比较简单但是也比较关键,我完成kts的改造和顺利的引入ksp大概花了一个周末的时间,虽然有一部分时间我在带娃。
首先在项目的根目录下settings.gralde.kts
文件下加入如下代码,划重点哦是settings.gradle.kts
哦。
pluginManagement {
repositories {
gradlePluginPortal()
google()
maven("https://dl.bintray.com/kotlin/kotlin-eap")
}
}
这个会帮助我们去获取ksp plugin
的依赖。同时由于ksp
的依赖于kotlin plugin 1.4.30
版本,所以我们要对kt插件版本也进行一次升级。
build.gradle.kts
下添加如下代码即可。
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath(kotlin("gradle-plugin", version = "1.4.30"))
}
}
plugins {
// 生命kotlin 1.4.30 插件版本 但是并不直接引入
kotlin("jvm") version "1.4.30" apply false
}
第二步
完成了第一步的准备工作之后,接下来我们可以创建一个kspCompiler
的模块。然后我们可以开启ksp
编写之旅了。
先简单看下我们的目录下的build.gradle
。因为我们要获取到ksp的依赖,同时ksp
和kapt
一样,都是通过SPI的机制进行加载的,所以我们本身也要引入一个ksp的注解库。
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
plugins {
// 这其实就是个java 的plugin
kotlin("jvm")
// 因为我们要用ksp生成一个META-INF 所以也需要ksp插件
id("com.google.devtools.ksp") version "1.4.30-1.0.0-alpha04"
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:${getKotlinPluginVersion()}")
implementation(project(":RouterAnnotation"))
implementation("com.squareup:kotlinpoet:1.7.2")
implementation("com.google.auto.service:auto-service-annotations:1.0-rc7")
//一定要加这个而且必须compileOnly 否则会出问题
compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:${getKotlinPluginVersion()}")
compileOnly("dev.zacsweers.autoservice:auto-service-ksp:0.3.2")
compileOnly("com.google.devtools.ksp:symbol-processing-api:1.4.30-1.0.0-alpha04")
// 生成META-INF 其实你可以自定义一个META-INF文件 就不需要ksp或者apt来生成了
ksp("dev.zacsweers.autoservice:auto-service-ksp:0.3.2")
}
上面的标识了注释的地方各位一定要注意了,因为这几个点是我踩过的坑。其中compileOnly
会导致你的ksp无法被执行到,同时编译流程报错。
这边我个人的猜测是因为
implementation
只会对当前Module
生效,无法传递到别的地方,导致编译流程内无法引用到对应的jar包导致的,只是个人猜测没有去证实这部分猜测。
这里的ksp
就是负责生成的META-INF
文件夹的,编译流程中其实atuoservice其实还是有很多地方被使用到的。java中的jar包并不止有.class还有META-INFO
,这个文件夹会附带一些额外的信息。举个例子,koltin中的版本信息就是放在这个文件夹下面的。而我在上篇文章也说过autoservice的服务发现机制也是基于这个文件的。
所以,其实你在编写ksp
的时候,其实你也可以和编写plugin
一样自己生成一个META-INFO
也是可以的,文件名可以参考上面的图片。
第三步
到了这里我们可以正式的开始ksp
的代码编写了。首先我们要先实现一个SymbolProcessor
。
interface SymbolProcessor {
/**
* Called by Kotlin Symbol Processing to initialize the processor.
*
* @param options passed from command line, Gradle, etc.
* @param kotlinVersion language version of compilation environment.
* @param codeGenerator creates managed files.
*/
fun init(options: Map<String, String>, kotlinVersion: KotlinVersion, codeGenerator: CodeGenerator, logger: KSPLogger)
/**
* 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.
*/
fun process(resolver: Resolver): List<KSAnnotated>
/**
* 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() {}
}
ksp
算是kapt
的升级版,所以这部分注解解释器的实现的和AbstractProcessor
基本是一样的。init
方法获取构造的一些关键参数以及写入文件路径等等。process
则是让我们可以获取到当前的抽象语法树,之后获取到所有加了路由注解的相应的语法树,之后进行后续开发了。下面我们看下代码吧。
// 判断是否处理过了
private var isload = false
override fun process(resolver: Resolver): List<KSAnnotated> {
if (isload) {
return emptyList()
}
val symbols = resolver.getSymbolsWithAnnotation(BindRouter::class.java.name)
routerBindType = resolver.getClassDeclarationByName(
resolver.getKSNameFromString(BindRouter::class.java.name)
)?.asType() ?: kotlin.run {
logger.error("JsonClass type not found on the classpath.")
return emptyList()
}
symbols.asSequence().forEach {
add(it)
}
// logger.error("className:${moduleName}")
try {
ktGenerate.generateKt()
isload = true
} catch (e: Exception) {
logger.error(
"Error preparing :" " ${e.stackTrace.joinToString("n")}"
)
}
return symbols
}
首先我们可以先从resolver
中获取到我路由的直接的语法树列表,这就是我们后续需要处理的注解列表了。
小贴士 由于process中如果有类生成 所有会重新触发process。 因为语法树变更 由于路由的特性 所以这种多次的都不需要进行处理
接下来我们只要遍历循环这个symbols
列表,就可以继续路由表的生成了。
private fun add(type: KSAnnotated) {
logger.check(type is KSClassDeclaration && type.origin == Origin.KOTLIN, type) {
"@JsonClass can't be applied to $type: must be a Kotlin class"
}
if (type !is KSClassDeclaration) return
ktGenerate.addStatement(type, routerBindType)
//class type
// val id: Array<String> = routerAnnotation.urls()
}
然后我从KSAnnotated
中先判断当前的类型,判断当前语法树中是不是有java的注解,如果有则抛出异常。如果都满足的情况下,我们就可以进行代码记录的操作了。
class KtGenerate(private var logger: KSPLogger, name: String, private val codeGenerator: CodeGenerator) {
private val initMethod: FunSpec.Builder = FunSpec.builder("register").apply {
addAnnotation(AnnotationSpec.builder(JvmStatic::class).build())
}
private val className = "RouterInit${name.replace("[^0-9a-zA-Z_] ", "")}"
private val specBuilder = FileSpec.builder("com.kronos.router.register", className)
var index = 0
fun addStatement(type: KSClassDeclaration, routerBindType: KSType) {
val routerAnnotation = type.findAnnotationWithType(routerBindType) ?: return
var isRunnable = false
type.getAllSuperTypes().forEach {
if (!isRunnable) {
isRunnable = it.toClassName().canonicalName == RUNNABLE
}
}
// logger.error("classType:${isRunnable}")
val urls = routerAnnotation.getMember<ArrayList<String>>("urls")
if (urls.isEmpty()) {
return
}
val weight: Int = try {
routerAnnotation.getMember("weight")
} catch (e: Exception) {
0
}
val interceptors = try {
routerAnnotation.getMember<ArrayList<ClassName>>("interceptors")
} catch (e: Exception) {
null
}
urls.forEach { url ->
if (isRunnable) {
callBackStatement(url, type.toClassName(), weight, interceptors)
} else {
normalStatement(url, type.toClassName(), weight, interceptors)
}
index
}
}
private val optionClassName by lazy {
MemberName("com.kronos.router.model", "RouterOptions")
}
private val routerMemberName by lazy {
MemberName("com.kronos.router", "Router")
}
private fun callBackStatement(url: String, callBack: ClassName, weight: Int, interceptors: ArrayList<ClassName>?) {
val memberName = "option$index"
initMethod.addStatement("val $memberName = %M()", optionClassName)
buildInterceptors(memberName, interceptors)
initMethod.addStatement("$memberName.weight=$weight")
initMethod.addStatement("$memberName.callback=%T()", callBack)
initMethod.addStatement("%M.map(url=%S,options= $memberName)", routerMemberName, url)
}
private fun normalStatement(url: String, activity: ClassName, weight: Int, interceptors: ArrayList<ClassName>?) {
val memberName = "option$index"
initMethod.addStatement("val $memberName = %M()", optionClassName)
buildInterceptors(memberName, interceptors)
initMethod.addStatement("$memberName.weight=$weight")
initMethod.addStatement("%M.map(url=%S,mClass=%T::class.java,options= $memberName)",
routerMemberName, url, activity)
}
private fun buildInterceptors(memberName: String, interceptors: ArrayList<ClassName>?) {
interceptors?.forEach {
initMethod.addStatement("$memberName.addInterceptor(%T())", it)
}
}
fun generateKt() {
val helloWorld = TypeSpec.objectBuilder(className)
.addFunction(initMethod.build())
.build()
specBuilder.addType(helloWorld)
val spec = specBuilder.build()
val file = codeGenerator.createNewFile(Dependencies.ALL_FILES, spec.packageName, spec.name)
file.use {
val content = spec.toString().toByteArray()
it.write(content)
}
}
companion object {
const val RUNNABLE = "com.kronos.router.RouterCallback"
}
}
这部分就比较简单了,先判断类型是Activity
还是RouterCallbac
,然后根据不同的类型,插入不同的注册代码,只是相对于以前来说,这次我选择了KotlinPoet
。
最后,在完成循环之后只要完成generateKt
就可以完成kt类的生成了。但是各位老铁请一定注意我上面说的小tips。
如何接入
上面我们就基本完成了ksp
的解释器的开发了,那么就先看下如何在项目中使用吧。
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
plugins {
id("com.android.application")
id("com.google.devtools.ksp") version "1.4.30-1.0.0-alpha04"
}
dependencies {
ksp(project(":kspCompiler"))
}
简单的说就是只要加上一个和kapt类似的,这里顺便给各位不知道的科普个小知识。如果使用了这种ksp
相关的generate
的技术的话,可以在下图的目录中去查看是否有类生成。
耗时比较
我对其中一个module同时打开了ksp以及kapt,进行了数据测试。发现ksp的总耗时要比kapt要少。
kspDebugKotlin task
的任务耗时是1262ms,而kapt
因为被拆散成了两个任务,所以总耗时是1603ms。总体上来说,和谷歌说的25%的整体提速的预期还是符合的。
总结
希望这篇文章能帮助到各位,其实如果对编译速度要求比较高的项目,或者类似字节这种大厂。对于编译速度有要求,同时对kapt
当前的表现贼不满意的,就可以尝试下先行开发下kspCompiler
了,因为这部分都是类生成,所以ksp和kapt可以同时存在于项目中,进行灰度实验测试编译速度哦。