這一篇,咱們抱着擁抱新事物的心態,嘗試一些新事物。筆者在這一次歷程中,對三項事物進行了嚐鮮: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機制,又將這一問題擺上桌面,筆者也將思考並嘗試尋找一種 有效、有趣
的方式,來解決這一問題。若是各位讀者對此有比較好的點子,很是但願可以分享一二。