background

Hey, Android guys, your shrimp brother I’m back with the scroll.

Recently, I was busy with some things at work, so the update of the article was not timely (in fact, I was addicted to playing games), and some netizens said that I suddenly stopped writing. This is your carelessness, ha ha ha, secretly studying.

I didn’t want to use annotations to write the startup frame before, because the startup frame requires too many parameters. There is no difference between defining parameters in annotations and writing a task, so I never felt the need to use annotations.

But after talking with another colleague, if annotations can also be permutations and combinations, it seems that annotations can be used to solve this problem, and I feel that it will be very fun to use them.

Open book open book

The first thing we need to do is define the annotation we want. We can define the annotation based on our previous definition of task, such as whether it is the main thread, whether it needs to wait to complete, whether it depends on task, whether it is an anchor task, etc.

AndroidStartUp Demo address

// Whether to be asynchronous
@Async
// Whether to wait
@Await
// Anchor task
@MustAfter
// Dependencies
@DependOn(
    dependOn = [AsyncTask1Provider::class, SimpleTask2Provider::class],
    dependOnTag = ["taskB"])
Copy the code

The annotations above are the ones I defined as additional annotations, which I will use later to compose the Task I want to start.

Ksp parses annotations

Here I define a Startup annotation that identifies the current class as a started Task. Because in the compiler link of KSP or APT, we will first try to get all the annotation classes of the current syntax tree.

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(// process strategy val strategy: Process = process. ALL, val processName: Array<String> = [])Copy the code

I’m going to start with the demo and work through how I’m going to write this stuff. Here is a simple start Task defined by me. The specific content of Task should be generated by 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") } }Copy the code

As I said at the beginning, our first entry point is the Startup annotation, and then we get the abstract syntax tree information for SampleGenerate1Task, and then we move on from there.

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)) }Copy the code

Based on the KSClassDeclaration syntax tree, we can retrieve the annotations on the current class and then generate the corresponding start task after the collection is complete. First, we need to get all the annotations on the class, and then iterate through them, adjusting the data structure information when the current annotation fits the type we want.

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 }Copy the code

Next we use a data structure to collect this annotation information, and then do what we did in the previous article. We will do the code generation logic in the Finish method after the annotation information has been collected. Interested students can take a look at GenerateTaskKt, the logic is relatively simple, based on the data structure to insert different KT code logic.

    //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)
           }
         }
       }
Copy the code

Code links interested can look at

Task generation also incorporates the TaskGroup concept

Since our previous assumption was to return a group of tasks to StartupTaskProcessGroup, this part of the code uploaded to the Task also needs to be consistent.

All we need to do is bring the Task class information generated by KSP into the TaskGroup generation logic.

Since we had a better base, we can do this by simply inserting the information generated by these classes into the original 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)
               }
Copy the code

In fact, we already inserted code into the list in the Task traversal above. This will allow for the subsequent insertion logic.

Split startup steps

The next thing I want to talk about is another concept, because there’s a lot of demand for privacy compliance, so one of the things that most companies need to do is separate the pre-privacy initialization logic from the post-privacy initialization logic.

This is where THE step thing I want to talk about comes in, so we need to redefine a new annotation.

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

Copy the code

This is the concept of step by step that I imagined. I set up three different stages in the demo, corresponding to Attach and CREATE of application respectively, as well as the code after privacy consent.

This time, I’ll add something to the task. I want a Module to export an array of startupTaskProcessGroups for all the stages, which I’ll call StepTaskBuilder.

We can then collect all the stepTaskBuilders of the modules together to complete the automatic initialization task. One advantage of this is that the subsequent dependencies can be completed in the compile packaging phase, and the code only needs to add calls.

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

I added this paragraph at the end of the Finish method to generate the StepTaskBuilder. Logic is also relatively simple, you take a look at it.

Dependency injection

I know you’re all confused by this subtitle. Why does a broken startup framework need dependency injection logic?

Normally, when writing an SDK, there are many initialization parameters that need to be defined by the user, such as okHTTP timeout, cache path, thread size, etc.

The same thing would happen in the startup framework. What we want to do is an automatic initialization framework, so the parameters of these changes need to be injected.

The dependency injection completed by Koin in demo can basically satisfy my demands by flipping the dependency to the outermost layer and setting the changing part by APP.

The implementation class Settings in application are as follows.

val appModule = module {
    single<ReportInitDelegate> {
        ReportInitDelegateImp()
    }
}

private class ReportInitDelegateImp : ReportInitDelegate {
    override fun getAppName(): String {
        return "123444556"
    }

}
Copy the code

In the SDK module initialization through dependency injection abstract interface implementation, so that the SDK can directly initialize the code. Some SDK initialization order issues can be circumvent.

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

}
Copy the code

TODO

There is also a function to collect StepTaskBuilder in all modules which needs to be added a Plugin. After collecting all modules in the compilation stage, corresponding functions can be basically completed.

conclusion

I think this part is quite interesting. Our original design is to complete the construction of all shell projects through this automatic startup framework, so that the developers can feel the logic related to the startup process as little as possible.