这一篇,咱们抱着拥抱新事物的心态,尝试一些新事物。笔者在这一次历程中,对三项事物进行了尝鲜:html
Groovy Script
转为 Kotlin Script
此次的 重点是KSP
,Kotlin Poet学习成本比较低,迁移 Kotlin Script
仅仅是比较繁琐。java
既然要实战,那么就须要一个实际的项目来支持,笔者选取了我的的开源项目DaVinCi,.android
关注笔者动态的读者可能注意到:笔者在过年时发布过一篇文章好玩系列:拥有它,XML文件少一半--更方便的处理View背景, 在这篇文章中,咱们提到了一种 取代xml背景资源文件
的方案,而且提到以后会 实现Style机制
。本篇文章中,以此为目标,展开 KSP的实战过程
。git
PS:对DaVinCi不了解并不影响本文内容的理解github
若是读者对好玩系列感兴趣,建议点个关注,查看 关于好玩系列 了解笔者创做该系列的初衷。express
在正式了解KSP以前,咱们须要 复习
以前的知识:api
由于一些特定的需求,在项目进行编译的过程当中,须要增长必定的处理,例如:生成源码文件并参与编译;修改编译的产物。markdown
基于Gradle编译任务链中的 APT机制
,能够实现 Annotation Processor
,常见于 代码生成
, SPI机制实现
如AutoService ,也能够用来生成文档。架构
而APT仅支持Java源码,KAPT并无 专门的注解处理器
,因此kotlin项目使用KAPT时,须要 生成代码桩
即Java Stub 再交由APT 进行处理。app
基于Gradle编译任务链中的 Transformer机制
,能够动态的修改编译结果,例如利用 Javasist
,ASM
等字节码操纵框架 增长、修改字节码中的业务逻辑。
这致使Kotlin项目想要针对注解进行处理时,要么用力过猛,采用Transformer机制,要么就使用KAPT并牺牲时间。Transformer机制并没有时间优点,若KAPT能够等价处理时, Transformer机制每每呈现力大砖飞之势
那么瓜熟蒂落,KSP用于解决纯Kotlin项目下,无专门注解处理器的问题。
在KSP以前,Kotlin的编译存在有 Kotlin Compiler Plugin
了解更多 ,下文简称KCP,KCP用于解决Kotlin 的关键词和注解的编译问题,例如 data class
,
而KCP的功能太过于强大,以致于须要 很大的学习成本
,而将问题局限于 注解处理
时,这一学习成本是多余的,因而出现了KSP,它基于KCP,但 屏蔽了KCP的细节
, 让咱们 专一于注解处理的业务
KCP的复杂程度从其架构可见一斑
在正式开始以前,咱们再简要的阐明一下实战的目标:DaVinCi中能够定义 Style 和 StyleFactory:
推荐使用 StyleRegistry.Style.Factory,而不要直接定义 StyleRegistry.Style
@DaVinCiStyle(styleName = "btn_style.main")
class DemoStyle : StyleRegistry.Style("btn_style.main") {
init {
this.register(
state = State.STATE_ENABLE_FALSE,
expression = DaVinCiExpression.shape().rectAngle().solid("#80ff3c08").corner("10dp")
).register(
state = State.STATE_ENABLE_TRUE,
expression = DaVinCiExpression.shape().rectAngle().corner("10dp")
.gradient("#ff3c08", "#ff653c", 0)
)
}
}
@DaVinCiStyleFactory(styleName = "btn_style.main")
class DemoStyleFactory : StyleRegistry.Style.Factory() {
override val styleName: String = "btn_style.main"
override fun apply(style: StyleRegistry.Style) {
style.register(
state = State.STATE_ENABLE_FALSE,
expression = DaVinCiExpression.shape().rectAngle().solid("#80ff3c08").corner("10dp")
).register(
state = State.STATE_ENABLE_TRUE,
expression = DaVinCiExpression.shape().rectAngle().corner("10dp")
.gradient("#ff3c08", "#ff653c", 0)
)
}
}
复制代码
并利用
osp.leobert.android.davinci.StyleRegistry#register(style: Style)
osp.leobert.android.davinci.StyleRegistry#registerFactory(factory: Style.Factory)
复制代码
进行全局注册
咱们而且指望将 StyleName
生成常量,且分别检查Style和StyleFactory是否有重复。
那么咱们指望生成如下内容:
/** * auto-generated by DaVinCi, do not modify */
public object AppDaVinCiStyles {
public const val btn_style_main: String = "btn_style.main"
/** * register all styles and styleFactories */
public fun register(): Unit {
registerStyles()
registerStyleFactories()
}
private fun registerStyles(): Unit {
osp.leobert.android.davinci.StyleRegistry.register(com.example.simpletest.factories.DemoStyle())
}
private fun registerStyleFactories(): Unit {
osp.leobert.android.davinci.StyleRegistry.registerFactory(com.example.simpletest.factories.DemoStyleFactory())
}
}
复制代码
@Target(AnnotationTarget.CLASS)
public annotation class DaVinCiStyle(val styleName: String)
@Target(AnnotationTarget.CLASS)
public annotation class DaVinCiStyleFactory(val styleName: String)
复制代码
显然,须要单独创建Module,这并不复杂。
//project build.gradle.kts
plugins {
id("com.google.devtools.ksp") version Dependencies.Kotlin.Ksp.version apply false
kotlin("jvm") version Dependencies.Kotlin.version apply false
id("org.jetbrains.dokka") version Dependencies.Kotlin.dokkaVersion apply false
id("com.vanniktech.maven.publish") version "0.15.1" apply false
}
复制代码
//Module build.gradle.kts
plugins {
id("com.google.devtools.ksp") //使用了ksp版本的autoservice,故而此处也须要使用ksp
kotlin("jvm")
}
dependencies {
compileOnly(Dependencies.Kotlin.Ksp.api)
implementation(Dependencies.AutoService.annotations)
ksp("dev.zacsweers.autoservice:auto-service-ksp:0.5.2")
implementation(Dependencies.KotlinPoet.kotlinPoet)
implementation(Dependencies.guava)
// todo use stable version when release
implementation(project(":annotation"))
//略
}
复制代码
引入必要的依赖,这里咱们使用ksp实现的auto-service实现SPI,具体可参考DaVinCi项目源码,此处再也不赘述
核心很是简要,实现SymbolProcessorProvider接口
,提供一个 SymbolProcessor
接口的实例
package com.google.devtools.ksp.processing
/** * [SymbolProcessorProvider] is the interface used by plugins to integrate into Kotlin Symbol Processing. */
interface SymbolProcessorProvider {
/** * Called by Kotlin Symbol Processing to create the processor. */
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}
复制代码
处理注解的入口:
package com.google.devtools.ksp.processing
import com.google.devtools.ksp.symbol.KSAnnotated
/** * [SymbolProcessor] is the interface used by plugins to integrate into Kotlin Symbol Processing. * SymbolProcessor supports multiple round execution, a processor may return a list of deferred symbols at the end * of every round, which will be passed to proceesors again in the next round, together with the newly generated symbols. * Upon Exceptions, KSP will try to distinguish the exceptions from KSP and exceptions from processors. * Exceptions will result in a termination of processing immediately and be logged as an error in KSPLogger. * Exceptions from KSP should be reported to KSP developers for further investigation. * At the end of the round where exceptions or errors happened, all processors will invoke onError() function to do * their own error handling. */
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. */
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() {}
}
复制代码
环境能够给到的内容:
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
)
复制代码
这里注意,ksp还没法像APT同样进行debug,因此开发阶段还有一些障碍,仅能 靠日志进行排查
。
codeGenerator
用于生成kotlin源码文件,注意写入时 必须分离到子线程
,不然KSP会进入无限等待。 options
用于手机、获取配置参数
略去获取配置参数的部分
定义目标注解的信息:
val DAVINCI_STYLE_NAME = requireNotNull(DaVinCiStyle::class.qualifiedName)
val DAVINCI_STYLE_FACTORY_NAME = requireNotNull(DaVinCiStyleFactory::class.qualifiedName)
复制代码
利用Resolver获得目标注解的KSName,e.g.:
resolver.getKSNameFromString(DAVINCI_STYLE_NAME)
复制代码
并进一步获得被注解的类
resolver.getClassDeclarationByName(
resolver.getKSNameFromString(DAVINCI_STYLE_NAME)
)
复制代码
此刻,代码示例以下:
private class DaVinCiSymbolProcessor(
environment: SymbolProcessorEnvironment,
) : SymbolProcessor {
//忽略
companion object {
val DAVINCI_STYLE_NAME = requireNotNull(DaVinCiStyle::class.qualifiedName)
val DAVINCI_STYLE_FACTORY_NAME = requireNotNull(DaVinCiStyleFactory::class.qualifiedName)
}
override fun process(resolver: Resolver): List<KSAnnotated> {
val styleNotated = resolver.getClassDeclarationByName(
resolver.getKSNameFromString(DAVINCI_STYLE_NAME)
)?.asType(emptyList())
val factoryNotated = resolver.getClassDeclarationByName(
resolver.getKSNameFromString(DAVINCI_STYLE_FACTORY_NAME)
)?.asType(emptyList())
//暂未涉及
return emptyList()
}
}
复制代码
进行必要的检查,若是没有任意的目标注解,按照本身的计划进行抛错或者其余;
此刻咱们获得了目标注解的类型,即 KSClassDeclaration实例
利用 Resolver
和 目标注解的类型
进行扫描
而咱们的既定目标是寻找被注解的类,因此直接过滤被注解的目标为 KSClassDeclaration
,直接排除掉 Method 和 Property
factoryNotated?.let {
handleDaVinCiStyleFactory(resolver = resolver, notationType = it)
}
private fun handleDaVinCiStyleFactory(resolver: Resolver, notationType: KSType) {
resolver.getSymbolsWithAnnotation(DAVINCI_STYLE_FACTORY_NAME)
.asSequence()
.filterIsInstance<KSClassDeclaration>()
.forEach { style ->
//解析类上的注解信息、保存以待后续处理
/*end @forEach*/
}
}
复制代码
这里和APT有一点差别,没法直接将 KSAnnotation
转换为实际注解,但并不影响咱们操做,能够判断注解的类型、获取注解中方法的返回值。
前面已经获得了被注解的 KSClassDeclaration实例
,能够直接获得对于它的注解,并经过 KSType
比对获得目标注解,并进一步解析其 arguments
, 获得注解中的值。
val annotation =
style.annotations.find { it.annotationType.resolve() == notationType }
?: run {
logE("@DaVinCiStyleFactory annotation not found", style)
return@forEach
}
//structure: DaVinCiStyle(val styleName: String, val parent: String = "")
val styleName = annotation.arguments.find {
it.name?.getShortName() == "styleName"
}?.value?.toString() ?: kotlin.run {
logE("missing styleName? version not matched?", style)
return@forEach
}
//下面是简要的信息处理和保存
val constName = generateConstOfStyleName(styleName)
if (styleFactoryProviders.containsKey(constName)) {
logE(
"duplicated style name:${styleName}, original register:${styleFactoryProviders[constName]?.clzNode}",
style
)
return@forEach
}
styleFactoryProviders[constName] = MetaInfo.Factory(
constName = constName,
styleName = styleName,
clzNode = style
)
复制代码
参考 kotlin poet 进行学习
鉴于此部分代码彻底与DaVinCi的业务相关,故略去。
//daVinCiStylesSpec 为利用Kotlin Poet编写的源码信息
val fileSpec = FileSpec.get(packageName ?: "", daVinCiStylesSpec)
val dependencies = Dependencies(true)
thread(true) {
codeGenerator.createNewFile(
dependencies = dependencies,
packageName = packageName ?: "",
fileName = "${moduleName ?: ""}DaVinCiStyles",
extensionName = "kt"
).bufferedWriter().use { writer ->
try {
fileSpec.writeTo(writer)
} catch (e: Exception) {
logE(e.message ?: "", null)
} finally {
writer.flush()
writer.close()
}
}
}
复制代码
再次注意,须要在子线程中执行
若对碎片化的代码不太敏感,能够下载DaVinCi的源码进行对照阅读
前面咱们已经完成了KSP的核心逻辑,如今咱们须要配置并使用它
这里注意,ksp的生成目录不属于默认sourceSets,须要单独配置
plugins {
id("com.android.application")
id("com.google.devtools.ksp") //version Dependencies.Kotlin.Ksp.version
//略
}
android {
//略
buildTypes {
getByName("release") {
sourceSets {
getByName("main") {
java.srcDir(File("build/generated/ksp/release/kotlin"))
}
}
//略
}
getByName("debug").apply {
sourceSets {
getByName("main") {
java.srcDir(File("build/generated/ksp/debug/kotlin"))
}
}
}
}
}
ksp {
arg("daVinCi.verbose", "true")
arg("daVinCi.pkg", "com.examole.simpletest")
arg("daVinCi.module", "App")
}
dependencies {
implementation(project(":davinci"))
ksp(project(":anno_ksp"))
implementation(project(":annotation"))
//略
}
复制代码
运行 kspXXXXKotlin
任务便可
至此,KSP的实战已告一段落,相信读者朋友们必定产生了浓厚的兴趣,并准备尝试一番了,赶忙开始吧!
继DaVinCi开源以及相关文章发布后,也引发了部分读者朋友们的讨论和关注,这次结合实战KSP的机会,对DaVinCi的功能进行了升级,这里也简单的交代一下, DaVinCi目前已经支持:
GradientDrawable
, StateListDrawable
的背景设置 (原有)ColorStateList
文字色设置(新增)开发DaVinCi时,个人初衷是:"既然难以管理Style和Shape资源,那索性就用一种更加方便的方式来处理",但这件自己是违背"优秀代码、优秀项目管理"的。
而本次为DaVinCi添加Style机制,又将这一问题摆上桌面,笔者也将思考并尝试寻找一种 有效、有趣
的方式,来解决这一问题。若是各位读者对此有比较好的点子,很是但愿可以分享一二。