preface

When we develop applications, we usually introduce SDKS, and most SDKS require us to initialize in the Application. As we introduce more and more SDKS, the Application will become longer and longer. If the initialization tasks of SDKS are interdependent, There’s a lot of conditional judgment to deal with, and at this point, if we do another asynchronous initialization, we’re going to crash.

Some people might say, well, I’m just going to initialize everything sequentially on the main thread, sure, as long as the boss doesn’t bother you

“Xiao Wang, why did it take so long for our APP to start?”

Just kidding, you can see how important a good startup framework is for APP startup performance.

Why not use Google StartUp?

When it comes to StartUp frameworks, we have to mention StartUp. After all, it is officially produced by Google. The existing StartUp frameworks are more or less referred to as StartUp. Check out this article in the Jetpack series on App Startup from Startup to Startup

StartUp provides easy dependency task initialization, but for a complex project, StartUp has the following disadvantages

  1. Asynchronous tasks are not supported

Started with the ContentProvider, all tasks are executed on the main thread, and started with the interface, all tasks are executed on the same thread

  1. Componentization is not supported

Using Class to specify the dependent task, you need to reference the dependent module

  1. Multiple processes are not supported

You cannot configure the process that a task needs to execute

  1. Startup priority is not supported

Although it is possible to set priorities by specifying dependencies, it is too complex

What does a proper startup framework look like?

  1. Support for asynchronous tasks

Effective means to reduce start-up time

  1. Componentization is supported

This is decoupling, on the one hand, of task dependencies, and on the other hand, of app and Module dependencies

  1. Support task dependencies

It simplifies our task scheduling

  1. Supported priority

Allow tasks to take precedence without dependencies

  1. Support for multiple processes

Only perform initialization tasks in required processes, which can reduce system load and improve APP startup speed

Collect task

To achieve complete decoupling, we can use APT collection tasks

First you define annotations, which are some attributes of the task

@ Target (AnnotationTarget. CLASS) @ Retention (AnnotationRetention. RUNTIME) the annotation CLASS InitTask (/ * * * task name, */ val background: Boolean = false, /** * priority: Int = PRIORITY_NORM, /** * task execution process, supporting main process, non-main process, all process, : XXX, specific process name */ val process: Array<String> = [PROCESS_ALL], /** ** dependent task */ val depends: Array<String> = [])Copy the code
  • nameAs a unique task identifier, String is used to decouple task dependencies
  • backgroundThat is, whether to run it in the background
  • priorityIs the execution order in the main thread, no dependency scenario
  • processSpecifies the process in which a task is executed. It can be the main process, non-main process, all processes, : XXX, or specific process name
  • dependsSpecify dependent tasks

Once the attributes of the task are defined, you also need an interface to execute the task

interface IInitTask {
    fun execute(application: Application)
}
Copy the code

The information that a task needs to collect has been defined, so what does a real task look like

@InitTask(
    name = "main",
    process = [InitTask.PROCESS_MAIN],
    depends = ["lib"]
)
class MainTask : IInitTask {
    override fun execute(application: Application) {
        SystemClock.sleep(1000)
        Log.e("WCY", "main1 execute")
    }
}
Copy the code

It’s pretty neat and clear

Next you need to collect tasks via the Annotation Processor and then write files via Kotlin Poet

class TaskProcessor : AbstractProcessor() { override fun process(annotations: MutableSet<out TypeElement>? , roundEnv: RoundEnvironment): Boolean { val taskElements = roundEnv.getElementsAnnotatedWith(InitTask::class.java) val taskType = elementUtil.getTypeElement("me.wcy.init.api.IInitTask") /** * Param type: MutableList<TaskInfo> * * There's no such type as MutableList at runtime so the library only sees the runtime type. * If  you need MutableList then you'll need to use a ClassName to create it. * [https://github.com/square/kotlinpoet/issues/482] */ val inputMapTypeName = ClassName("kotlin.collections", "MutableList").parameterizedBy(TaskInfo::class.asTypeName()) /** * Param name: taskList: MutableList<TaskInfo> */ val groupParamSpec = ParameterSpec.builder(ProcessorUtils.PARAM_NAME, inputMapTypeName).build() /** * Method: override fun register(taskList: MutableList<TaskInfo>) */ val loadTaskMethodBuilder = FunSpec.builder(ProcessorUtils.METHOD_NAME) .addModifiers(KModifier.OVERRIDE) .addParameter(groupParamSpec) for (element in taskElements) { val typeMirror = element.asType() val task = element.getAnnotation(InitTask::class.java) if (typeUtil.isSubtype(typeMirror, taskType.asType())) { val taskCn = (element as TypeElement).asClassName() /** * Statement: taskList.add(TaskInfo(name, background, priority, process, depends, task)); */ loadTaskMethodBuilder.addStatement( "%N.add(%T(%S, %L, %L, %L, %L, %T()))", ProcessorUtils.PARAM_NAME, TaskInfo::class.java, task.name, task.background, task.priority, ProcessorUtils.formatArray(task.process), ProcessorUtils.formatArray(task.depends), taskCn ) } } /** * Write to file */ FileSpec.builder(ProcessorUtils.PACKAGE_NAME, "TaskRegister\$$moduleName") .addType( TypeSpec.classBuilder("TaskRegister\$$moduleName") .addKdoc(ProcessorUtils.JAVADOC) .addSuperinterface(ModuleTaskRegister::class.java) .addFunction(loadTaskMethodBuilder.build()) .build() ) .build() .writeTo(filer) return true } }Copy the code

What does the generated file look like

public class TaskRegister$sample : ModuleTaskRegister {
  public override fun register(taskList: MutableList<TaskInfo>): Unit {
    taskList.add(TaskInfo("main2", true, 0, arrayOf("PROCESS_ALL"), arrayOf("main1","lib1"),MainTask2()))
    taskList.add(TaskInfo("main3", false, -1000, arrayOf("PROCESS_ALL"), arrayOf(), MainTask3()))
    taskList.add(TaskInfo("main1", false, 0, arrayOf("PROCESS_MAIN"), arrayOf("lib1"), MainTask()))
  }
}
Copy the code

The sample module collects three tasks, and TaskInfo aggregates the task information.

We know that APT can generate code, but we can’t modify bytecode, which means that we need to get the injected tasks at run time, and also need to inject the collected tasks into the source code.

Here we can use AutoRegister to help us with the injection.

Before the injection

internal class FinalTaskRegister {
    val taskList: MutableList<TaskInfo> = mutableListOf()

    init {
        init()
    }

    private fun init() {}

    fun register(register: ModuleTaskRegister) {
        register.register(taskList)
    }
}
Copy the code

The collected tasks are injected into the init method, the injected bytecode

/* compiled from: FinalTaskRegister.kt */ public final class FinalTaskRegister { private final List<TaskInfo> taskList = new ArrayList(); public FinalTaskRegister() { init(); } public final List<TaskInfo> getTaskList() { return this.taskList; } private final void init() { register(new TaskRegister$sample_lib()); register(new TaskRegister$sample()); } public final void register(ModuleTaskRegister register) { Intrinsics.checkNotNullParameter(register, "register"); register.register(this.taskList); }}Copy the code

The classes we generated through APT have been successfully injected into the code.

summary

At this point, we have completed the collection of tasks, through APT and bytecode modification is a common class collection scheme, bytecode modification has no performance loss compared to reflection.

Task scheduling

Task scheduling is at the heart of the startup framework, as you may have heard

The first step in dealing with dependent tasks is to build a directed acyclic graph

What is directed acyclic graph, wikipedia

In Graph theory, a Directed Acyclic Graph is a DAG (Directed Acyclic Graph) if it starts from any vertex and cannot return to that point by several edges.

Sounds like very simple, so how to achieve it, today we put aside advanced concepts, with code to achieve the task scheduling.

First, you need to divide tasks into two categories, dependent tasks and non-dependent tasks.

If there is a loop dependency, throw it directly. This can be used by using the formula “How to determine if the list has a loop”.

If there are no cyclic dependencies, the dependent tasks of each task are collected, which are called “sub-tasks” for continuing the sub-tasks after the current task is completed.

No dependency is the simplest, directly according to the priority can be executed.

I don’t know if you have any questions: when do dependent tasks start?

A dependent task. The leaf endpoint of a dependent chain must be an undependent task. Therefore, after the undependent task is completed, the dependent task can be started.

Here is a small example

  • ARely onB,C
  • BRely onC
  • CWithout relying on

A tree structure

  1. Group and comb through subtasks
  • A dependent
    • A: No subtask
    • B: Subtask: [A]
  • Without relying on
    • C: Subtask: [A.B]

  1. Perform tasks without dependenciesC
  2. Update completed tasks: [C]
  3. checkCWhether the subtask of
  • A: rely on [B.C] is not included in completed tasksB, cannot start
  • B: rely on [C] is included in the completed taskC, can be executed
  1. Perform a taskB
  2. Repeat Step 3 until all tasks are complete

So let’s do that in code

Use recursion to check for loop dependencies

private fun checkCircularDependency( chain: List<String>, depends: Set<String>, taskMap: Map<String, TaskInfo> ) { depends.forEach { depend -> check(chain.contains(depend).not()) { "Found circular dependency chain: $chain -> $depend" } taskMap[depend]? .let { task -> checkCircularDependency(chain + depend, task.depends, taskMap) } } }Copy the code

Comb subtask

task.depends.forEach {
    val depend = taskMap[it]
    checkNotNull(depend) {
        "Can not find task [$it] which depend by task [${task.name}]"
    }
    depend.children.add(task)
}
Copy the code

Perform a task

private fun execute(task: TaskInfo) {
    if (isMatchProgress(task)) {
        val cost = measureTimeMillis {
            kotlin.runCatching {
                (task.task as IInitTask).execute(app)
            }.onFailure {
                Log.e(TAG, "executing task [${task.name}] error", it)
            }
        }
        Log.d(
            TAG, "Execute task [${task.name}] complete in process [$processName] " +
                    "thread [${Thread.currentThread().name}], cost: ${cost}ms"
        )
    } else {
        Log.w( TAG, "Skip task [${task.name}] cause the process [$processName] not match")
    }
    afterExecute(task.name, task.children)
}
Copy the code

If the process does not match, skip it

Move on to the next task

private fun afterExecute(name: String, children: Set<TaskInfo>) { val allowTasks = synchronized(completedTasks) { completedTasks.add(name) children.filter { CompletedTasks. ContainsAll (it depends)}} the if (ThreadUtils. IsInMainThread ()) {/ / if it is the main thread, asynchronous task first in the queue, Allowtasks.filter {it. Background}. ForEach {launch(dispatchers.default) {execute(it)}} allowtasks.filter {it. it.background.not() }.forEach { execute(it) } } else { allowTasks.forEach { val dispatcher = if (it.background) Dispatchers.Default else Dispatchers.Main launch(dispatcher) { execute(it) } } } }Copy the code

If the dependent tasks of the subtask have all been executed, it can be executed

Finally, you need to provide an interface to start the task. In order to support multiple processes, you cannot use a ContentProvider.

summary

Through layer upon layer disassembling, the complex dependency is sorted out clearly, and the task scheduling is realized in an easy-to-understand way.

The source code

Github.com/wangchenyan…

In addition, I have also released an alpha version on JitPack, so you are welcome to try it out

kapt "com.github.wangchenyan.init:init-compiler:1-alpha.1"
implementation "com.github.wangchenyan.init:init-api:1-alpha.1"
Copy the code

For details, go to GitHub

conclusion

In this paper, StartUp is used as an introduction to explain what capabilities are needed to rely on the task StartUp framework. Decoupling is carried out through APT + bytecode injection, modularization is supported, and specific implementation methods of task scheduling are expressed through a simple model.

Hopefully this article has given you an idea of the task-dependent startup framework, and if you have any suggestions, please feel free to comment.

reference

Kotlin + Flow implementation of Android application initialization task start library