Android開發註解排列組合出啟動任務ksp

背景

之前我不想用註解來寫啟動框架,因為啟動框架需要的參數太多瞭。將參數都定義在註解內和寫一個task就沒有本質上的差別,所以一直覺得沒必要用註解來搞。

但是之前和另外一個同事聊瞭下,如果註解也可以進行排列組合,那麼貌似就可以用註解來解決這個問題咯,感覺這樣用起來就會很好玩瞭。

開卷開卷

首先要做的事情就是定義出我們想要的註解,可以基於我們之前對於task的定義來進行註解的定義,比如是不是主線程,是否需要等待完成,task的依賴任務,是否是錨點任務等等。

AndroidStartUp Demo地址

// 是否異步
@Async
// 是否等待
@Await
// 錨點任務
@MustAfter
// 依賴關系
@DependOn(
    dependOn = [AsyncTask1Provider::class, SimpleTask2Provider::class],
    dependOnTag = ["taskB"]
)

註解呢上面的這些就是我定義出來的新增的註解的,我後續會通過這些註解來組合出我所想要的啟動的Task。

Ksp解析註解

這裡我定義瞭一個Startup的註解,這個註解的目的就是標識當前的類是一個啟動的Task。因為在ksp或者aptcompiler環節上,都會先嘗試獲取到當前語法樹的所有註解的類。

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(
  // 進程策略
    val strategy: Process = Process.ALL,
    //進程名
    val processName: Array<String> = []
)

demo開始逐步推導我打算咋寫這些東西。下面是我定義的一個簡單的啟動任務,Task具體內容應該有apt來生成。

@Async
@Await
@MustAfter
@DependOn(
    dependOn = [AsyncTask1Provider::class, SimpleTask2Provider::class],
    dependOnTag = ["taskB"]
)
// 執行的進程名
@Startup(strategy = Process.MAIN)
class SampleGenerate1Task : TaskRunner {
    override fun run(context: Context) {
        info("SampleGenerate1Task")
    }
}

還是我一開始的說法,我們的第一個切入點是Startup註解,然後獲取到SampleGenerate1Task的抽象語法樹信息,之後再進行下一步操作。

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語法樹的信息,我們可以獲取到當前類上的註解,然後在收集完成之後再來生成對應的啟動任務。首先我們先要獲取到類上的所有的註解,然後進行遍歷,當當前註解符合我們所需要的類型情況下,調整數據結構信息就可以瞭。

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
  }

接下來我們用瞭一個數據結構來收集這些註解信息,然後和上篇文章說的一樣。我們會在註解信息收集完畢之後在finish方法進行代碼的生成邏輯。有興趣的同學可以自己看下GenerateTaskKt,邏輯相對來說比較簡單,基於數據結構插入不同的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生成還需要結合TaskGroup概念

因為我們之前的設想是回生成一個任務的分組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)
               }

其實我們在上面的Task遍歷的時候就已經對於這個list進行瞭代碼插入的操作瞭。這樣就能做到後續的插入邏輯瞭。

拆分啟動步驟

接下來我想說的就是另外一個概念瞭, 因為現在有很多隱私合規的訴求,所以大部分的公司都需要做一件事,就是把隱私前的初始化邏輯和隱私後的初始化邏輯進行拆分。

這也就有瞭我想說的分步驟的事情瞭,所以我們需要在重新定義一個新的註解出來。

@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")
}

這個就是我設想的分階段分步驟的概念,我在demo中設置瞭三個不同的階段,分別對應application的attach和create,還有隱私同意之後的代碼。

這次呢,我在上述task的基礎上又再次加瞭點東西進去,我希望一個module對外輸出的是一個包含瞭所有階段的StartupTaskProcessGroup的數組,我把它叫做StepTaskBuilder

之後我們隻要將所有模塊的StepTaskBuilder收集到一起,則就可以完成自動的初始化任務,這樣做的一個好處就是後續這種依賴關系就可以在編譯打包階段都完成瞭,代碼內隻需要加入調用就可以瞭。

val stageGenerator = StageGenerateKt(
             "${moduleName.upCaseKeyFirstChar()}StepBuilder",
             nameList,
             codeGenerator
         )
         stageGenerator.generateKt()

我在finish方法的最後面加入瞭這段,就是拿來生成StepTaskBuilder用的。邏輯也相對比較簡單,大傢自取看看就好瞭。

依賴註入

看到這個小標題,我知道各位都會有迷惑。為什麼一個破啟動框架還需要依賴註入的邏輯?

正常情況下,我們在寫sdk的時候,會有很多的初始化參數都需要使用方來定義的,比如okhttp的超時時間,緩存路徑,線程大小這類的變更的參數。

那麼同樣的情況也會出現在啟動框架內,我們想做的是一個能自動的初始化框架,那麼這部分變更的參數就需要被註入。

demo中使用koin來完成的依賴註入,將依賴翻轉到最外層,將變化的部分由app來設置,基本就能滿足我的訴求瞭。

application內的實現類設置具體的實現如下。

val appModule = module {
    single&lt;ReportInitDelegate&gt; {
        ReportInitDelegateImp()
    }
}
private class ReportInitDelegateImp : ReportInitDelegate {
    override fun getAppName(): String {
        return "123444556"
    }
}

sdk模塊初始化的時候通過依賴註入抽象接口的實現,這樣就可以再SDK直接進行初始化代碼的編寫瞭。可以規避掉一些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內的StepTaskBuilder功能沒寫,這部分需要增加一個Plugin,在編譯階段收集好所有,基本就可以完成對應的功能瞭。

總結

這部分我覺得還是挺有意思的,我們原來的設計上就是通過這種自動化的啟動框架,後續來完成所有殼工程的建設,讓開發同學盡量少感知到啟動流程相關的邏輯。

以上就是Android開發註解排列組合出啟動任務ksp的詳細內容,更多關於Android註解排列組合啟動任務ksp的資料請關註WalkonNet其它相關文章!

推薦閱讀: