Android開發(fā)注解排列組合出啟動任務(wù)ksp
背景
之前我不想用注解來寫啟動框架,因?yàn)閱涌蚣苄枰膮?shù)太多了。將參數(shù)都定義在注解內(nèi)和寫一個task就沒有本質(zhì)上的差別,所以一直覺得沒必要用注解來搞。
但是之前和另外一個同事聊了下,如果注解也可以進(jìn)行排列組合,那么貌似就可以用注解來解決這個問題咯,感覺這樣用起來就會很好玩了。
開卷開卷
首先要做的事情就是定義出我們想要的注解,可以基于我們之前對于task的定義來進(jìn)行注解的定義,比如是不是主線程,是否需要等待完成,task的依賴任務(wù),是否是錨點(diǎn)任務(wù)等等。

// 是否異步
@Async
// 是否等待
@Await
// 錨點(diǎn)任務(wù)
@MustAfter
// 依賴關(guān)系
@DependOn(
dependOn = [AsyncTask1Provider::class, SimpleTask2Provider::class],
dependOnTag = ["taskB"]
)
注解呢上面的這些就是我定義出來的新增的注解的,我后續(xù)會通過這些注解來組合出我所想要的啟動的Task。
Ksp解析注解
這里我定義了一個Startup的注解,這個注解的目的就是標(biāo)識當(dāng)前的類是一個啟動的Task。因?yàn)樵?code>ksp或者apt的compiler環(huán)節(jié)上,都會先嘗試獲取到當(dāng)前語法樹的所有注解的類。
package com.kronos.startup.annotation.startup
import com.kronos.startup.annotation.Process
/**
*
* @Author LiABao
* @Since 2021/12/31
*
*/
@Target(
AnnotationTarget.ANNOTATION_CLASS,
AnnotationTarget.CLASS
)
@Retention
annotation class Startup(
// 進(jìn)程策略
val strategy: Process = Process.ALL,
//進(jìn)程名
val processName: Array<String> = []
)
從demo開始逐步推導(dǎo)我打算咋寫這些東西。下面是我定義的一個簡單的啟動任務(wù),Task具體內(nèi)容應(yīng)該有apt來生成。
@Async
@Await
@MustAfter
@DependOn(
dependOn = [AsyncTask1Provider::class, SimpleTask2Provider::class],
dependOnTag = ["taskB"]
)
// 執(zhí)行的進(jìn)程名
@Startup(strategy = Process.MAIN)
class SampleGenerate1Task : TaskRunner {
override fun run(context: Context) {
info("SampleGenerate1Task")
}
}
還是我一開始的說法,我們的第一個切入點(diǎn)是Startup注解,然后獲取到SampleGenerate1Task的抽象語法樹信息,之后再進(jìn)行下一步操作。
private fun addStartUp(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
val startupAnnotation = type.findAnnotationWithType(startupType) ?: return
taskMap.add(StartupTaskBuilder(type, startupAnnotation))
}
基于KSClassDeclaration語法樹的信息,我們可以獲取到當(dāng)前類上的注解,然后在收集完成之后再來生成對應(yīng)的啟動任務(wù)。首先我們先要獲取到類上的所有的注解,然后進(jìn)行遍歷,當(dāng)當(dāng)前注解符合我們所需要的類型情況下,調(diào)整數(shù)據(jù)結(jié)構(gòu)信息就可以了。
class StartupTaskBuilder(type: KSClassDeclaration, startupAnnotation: KSAnnotation?) {
val className = type.toClassName()
var isAsync = false
var isAwait = false
var strategy: String
var processList: ArrayList<String> = arrayListOf()
val dependOnClassList = mutableListOf<ClassName>()
val dependOnStringList = mutableListOf<String>()
var mustAfter: Boolean = false
var lifecycle: Lifecycle = Lifecycle.OnApplicationCrate
init {
type.annotations.forEach {
val annotation = it.annotationType.resolve().toClassName()
if (annotation.canonicalName == "com.kronos.startup.annotation.startup.Async") {
isAsync = true
}
if (annotation.canonicalName == "com.kronos.startup.annotation.startup.Await") {
isAwait = true
}
if (annotation.canonicalName == "com.kronos.startup.annotation.startup.MustAfter") {
mustAfter = true
}
if (annotation.canonicalName == "com.kronos.startup.annotation.startup.DependOn") {
val value = it.getMember<ArrayList<ClassName>>("dependOn")
dependOnClassList.addAll(value)
val dependOnTag = it.getMember<ArrayList<String>>("dependOnTag")
dependOnStringList.addAll(dependOnTag)
}
if (annotation.canonicalName == "com.kronos.startup.annotation.Step") {
val value = it.arguments.firstOrNull {
it.name?.asString() == "lifecycle"
}?.value.toString().nameToLifeCycle()
lifecycle = value
mLogger?.warn("stage:$value")
}
}
type.getAllSuperTypes().forEach {
it.toClassName()
}
strategy = startupAnnotation?.arguments?.firstOrNull {
it.name?.asString() == "strategy"
}?.value.toString().toValue()
val list = startupAnnotation?.getMember<ArrayList<String>>("processName")
list?.let { processList.addAll(it) }
}
xxxxxxx
}
接下來我們用了一個數(shù)據(jù)結(jié)構(gòu)來收集這些注解信息,然后和上篇文章說的一樣。我們會在注解信息收集完畢之后在finish方法進(jìn)行代碼的生成邏輯。有興趣的同學(xué)可以自己看下GenerateTaskKt,邏輯相對來說比較簡單,基于數(shù)據(jù)結(jié)構(gòu)插入不同的kt代碼邏輯。
//SymbolProcessor
override fun finish() {
super.finish()
try {
val taskGenerate = GenerateTaskKt(taskMap, codeGenerator)
taskGenerate.procTaskGroupMap.forEach {
val list = procTaskGroupMap.getValueByDefault(it.key) {
mutableListOf()
}
list.addAll(it.value)
}
}
}
Task生成還需要結(jié)合TaskGroup概念
因?yàn)槲覀冎暗脑O(shè)想是回生成一個任務(wù)的分組StartupTaskProcessGroup,所以這部分代碼上傳的Task也需要保持一致。
我們需要做的就是將這些由ksp生成的Task類信息也帶到TaskGroup的生成邏輯中去。
由于我們之前的底子比較好,所以我們只要在將這些類生成的信息插入到原來的list中去則就可以完成這個操作了。
private val procTaskGroupMap =
hashMapOf<Lifecycle, MutableList<TaskBuilder>>()
val taskGenerate = GenerateTaskKt(taskMap, codeGenerator)
taskGenerate.procTaskGroupMap.forEach {
val list = procTaskGroupMap.getValueByDefault(it.key) {
mutableListOf()
}
list.addAll(it.value)
}
其實(shí)我們在上面的Task遍歷的時候就已經(jīng)對于這個list進(jìn)行了代碼插入的操作了。這樣就能做到后續(xù)的插入邏輯了。
拆分啟動步驟
接下來我想說的就是另外一個概念了, 因?yàn)楝F(xiàn)在有很多隱私合規(guī)的訴求,所以大部分的公司都需要做一件事,就是把隱私前的初始化邏輯和隱私后的初始化邏輯進(jìn)行拆分。
這也就有了我想說的分步驟的事情了,所以我們需要在重新定義一個新的注解出來。
@Target(
AnnotationTarget.ANNOTATION_CLASS,
AnnotationTarget.CLASS
)
@Retention
annotation class Step(val lifecycle: Lifecycle = Lifecycle.OnApplicationCrate)
enum class Lifecycle(val value: String) {
AttachApplication("AttachApplication"), OnApplicationCrate("OnApplicationCrate"),
AfterUserPrivacy("AfterUserPrivacy")
}
這個就是我設(shè)想的分階段分步驟的概念,我在demo中設(shè)置了三個不同的階段,分別對應(yīng)application的attach和create,還有隱私同意之后的代碼。
這次呢,我在上述task的基礎(chǔ)上又再次加了點(diǎn)東西進(jìn)去,我希望一個module對外輸出的是一個包含了所有階段的StartupTaskProcessGroup的數(shù)組,我把它叫做StepTaskBuilder。
之后我們只要將所有模塊的StepTaskBuilder收集到一起,則就可以完成自動的初始化任務(wù),這樣做的一個好處就是后續(xù)這種依賴關(guān)系就可以在編譯打包階段都完成了,代碼內(nèi)只需要加入調(diào)用就可以了。
val stageGenerator = StageGenerateKt(
"${moduleName.upCaseKeyFirstChar()}StepBuilder",
nameList,
codeGenerator
)
stageGenerator.generateKt()
我在finish方法的最后面加入了這段,就是拿來生成StepTaskBuilder用的。邏輯也相對比較簡單,大家自取看看就好了。
依賴注入
看到這個小標(biāo)題,我知道各位都會有迷惑。為什么一個破啟動框架還需要依賴注入的邏輯?
正常情況下,我們在寫sdk的時候,會有很多的初始化參數(shù)都需要使用方來定義的,比如okhttp的超時時間,緩存路徑,線程大小這類的變更的參數(shù)。
那么同樣的情況也會出現(xiàn)在啟動框架內(nèi),我們想做的是一個能自動的初始化框架,那么這部分變更的參數(shù)就需要被注入。
demo中使用koin來完成的依賴注入,將依賴翻轉(zhuǎn)到最外層,將變化的部分由app來設(shè)置,基本就能滿足我的訴求了。
application內(nèi)的實(shí)現(xiàn)類設(shè)置具體的實(shí)現(xiàn)如下。
val appModule = module {
single<ReportInitDelegate> {
ReportInitDelegateImp()
}
}
private class ReportInitDelegateImp : ReportInitDelegate {
override fun getAppName(): String {
return "123444556"
}
}
在sdk模塊初始化的時候通過依賴注入抽象接口的實(shí)現(xiàn),這樣就可以再SDK直接進(jìn)行初始化代碼的編寫了??梢砸?guī)避掉一些sdk的初始化順序問題。
@Async
@Await
@DependOn(dependOn = [NetworkSdkTaskProvider::class])
@Startup
class ReportSdkTask : KoinComponent, TaskRunner {
private val initDelegate: ReportInitDelegate by inject()
override fun run(context: Context) {
info("ReportSdkTask appName is:${initDelegate.getAppName()}")
}
}
TODO
還有個收集所有module內(nèi)的StepTaskBuilder功能沒寫,這部分需要增加一個Plugin,在編譯階段收集好所有,基本就可以完成對應(yīng)的功能了。
總結(jié)
這部分我覺得還是挺有意思的,我們原來的設(shè)計上就是通過這種自動化的啟動框架,后續(xù)來完成所有殼工程的建設(shè),讓開發(fā)同學(xué)盡量少感知到啟動流程相關(guān)的邏輯。
以上就是Android開發(fā)注解排列組合出啟動任務(wù)ksp的詳細(xì)內(nèi)容,更多關(guān)于Android注解排列組合啟動任務(wù)ksp的資料請關(guān)注腳本之家其它相關(guān)文章!
Android判斷NavigationBar是否顯示的方法(獲取屏幕真實(shí)的高度)
基于adbkit的android設(shè)備管理(精簡版stf)
Android入門之onTouchEvent觸碰事件的示例詳解
Android基礎(chǔ)之使用Fragment控制切換多個頁面
替換so文件來動態(tài)替換Flutter代碼實(shí)現(xiàn)詳解
Android項(xiàng)目實(shí)現(xiàn)視頻播放器
Android實(shí)現(xiàn)APP環(huán)境分離(利用Gradle)
Android webview如何加載HTML,CSS等語言的示例

