Android代码重构系列-02-使用Kotlin协程实现一个支持任务编排的轻量级启动器

前言

虽然本文的主题是启动器,但是笔者不打算去写怎么做启动优化,以及怎么实现一个完美的启动器。关于开源的第三方Android启动器已经有很多优秀的轮子了,比如阿里巴巴的alpha,参考 alpha 并改进其部分细节的Anchors,Start数比较高的android-startup,以及Android官方自己的app-startup等等。

本文的了灵感来源于我爱田Hebe应用程序启动优化新思路 - Kotlin协程,并参考了以上开源库做了一些改造,以便用起来更丝滑。本文主要分享笔者基于这个思路进行改造的几个点,强烈建议先阅读灵感来源的这篇文章,再继续往下阅读。

整体代码

先看下改造后的代码有一个初步的了解:

Android代码重构系列-02-使用Kotlin协程实现一个支持任务编排的轻量级启动器_第1张图片

XTask

任务类,主要是改了线程调度,增加优先级,增加Kotlin DSL相关方法,以及继承Observable。

/*
 * 任务
 */
data class XTask(
    val name: String,//任务名称,不可以重复
    val desc: String = "",//任务描述
    val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,//线程调度
    val priority: Int = 0,   // 运行的优先级,值小的先执行,
    var dependentTasks: Set = setOf(), // 依赖的任务集合(不可重复,所以用Set)
    val run: suspend () -> Boolean = { false }, //具体的执行任务的函数,为什么放在构造函数,主要是方便全局查看任务。返回是否拦截
) : Observable() {
    // 入度,用于有向无环图的拓扑排序
    var inDegree = dependentTasks.size

    /**
     * 通知通知观察者
     */
    fun notify(arg: Any) {
        setChanged()
        notifyObservers(arg)
    }

    /**
     * 构造任务集
     * @param tasks Array
     * @return MutableSet
     */
    fun on(vararg tasks: XTask): MutableSet {
        val dependentTasks = mutableSetOf()
        if (tasks.isNotEmpty()) {
            for (task in tasks) {
                dependentTasks.add(task)
            }
        }
        return dependentTasks
    }

    /**
     * 设置当前任务依赖的任务集
     * @param tasks Set
     * @return XTask
     */
    infix fun depends(tasks: Set): XTask {
        if (tasks.isNotEmpty()) {
            val dependentTasks = mutableSetOf()
            for (task in tasks) {
                dependentTasks.add(task.name)
            }
            this.dependentTasks = dependentTasks
            this.inDegree = this.dependentTasks.size
        }
        return this
    }

    /**
     * 设置当前任务依赖的任务集
     * @param block [@kotlin.ExtensionFunctionType] Function1>
     * @return String 返回任务名称
     */
    fun depends(block: XTask.() -> MutableSet): XTask {
        val tasks = block()
        if (tasks.isNotEmpty()) {
            val dependentTasks = mutableSetOf()
            for (task in tasks) {
                dependentTasks.add(task.name)
            }
            this.dependentTasks = dependentTasks
            this.inDegree = this.dependentTasks.size
        }
        return this
    }
}

fun  on(vararg elements: T): Set = if (elements.isNotEmpty()) elements.toSet() else emptySet()

XTaskProject

参考Anchors新增Project的定义,这样可以按项目的思维更灵活的定制我们的任务链。

/*
 * 任务项目,将一系列任务的集合当做一个任务工程来处理,这样每个功能模块都可以有自己的任务项目
 */
class XTaskProject(private val name: String) : IXTaskProject, XTaskLogger {

    /**
     * 为什么用map,利用Key-Value,判断同名的Task是否存在
     */
    private val taskMap = mutableMapOf()

    /**
     * 为什么用List,因为需要对Task进行排序,可以说是空间换时间
     */
    private val taskList: MutableList = mutableListOf()

    override fun addTask(task: XTask): IXTaskProject {
        //任务名称不能重复
        if (!taskMap.contains(task.name)) {
            logger.debug { "add Task $task" }
            taskMap[task.name] = task
        }
        return this
    }

    override fun getTasks(): List {
        // 根据优先级排序,值小的先执行
        return taskList.apply {
            clear()
            addAll(ArrayList(taskMap.values))
            sortBy { it.priority }
        }
    }

    override fun release() {
        logger.debug { "release" }
        taskMap.clear()
        taskList.clear()
    }
}

interface IXTaskProject {

    fun addTask(task: XTask): IXTaskProject

    fun getTasks(): List

    fun release()

}

XTaskStarter

任务启动器,主要是支持生成PlantUML Activity示意图。

package com.lonbon.lonbonxtask.core

import com.lonbon.lonbonxtask.core.logger.XTaskLogger
import com.lonbon.lonbonxtask.logger.PlantUMLCreator
import kotlinx.coroutines.*
import java.util.*

/*
 * *****************************************************************************
 * 

* Copyright (C),2007-2016, LonBon Technologies Co. Ltd. All Rights Reserved. *

* ***************************************************************************** * 任务启动器 */ object XTaskStarter : XTaskLogger { /** * 开始执行project中的任务 * runBlocking 会阻塞线程,如果是在主线程调用,则必须小心ANR * @param project IXTaskProject */ @JvmStatic fun start(project: IXTaskProject) = runBlocking { //获取按优先级排序的列表 val taskList: List = project.getTasks() //缓存完成的任务 val finishedTaskList: MutableList = mutableListOf() // 找出所有入度为0的任务,加入到队列当中 val queue: Queue = LinkedList() taskList.filter { it.inDegree == 0 }.forEach(queue::add) //建立一个map, 通过name可以获取协程的Job val jobMap = mutableMapOf() //循环执行队列里的任务 logger.info { "Start assigning tasks, current time ${System.currentTimeMillis()}" } var totalCostTime = 0L //添加第一个活动节点 val plantUMLCreator = PlantUMLCreator() plantUMLCreator.reset() if (queue.size > 1) { val parallelTasks = mutableListOf() for (task in queue) { parallelTasks.add(task.name) } plantUMLCreator.addParallelTask(parallelTasks) } else { queue.peek()?.let { plantUMLCreator.addSingleTask(it.name) } } while (queue.isNotEmpty()) { //获取当前需要执行的任务 val currentTask = queue.poll()!! logger.info { "--- Current assigned task is $currentTask" } //在指定的协程调度器启动一个协程,并返回Job jobMap[currentTask.name] = launch(currentTask.coroutineDispatcher) { //遍历当前任务依赖的任务集合 for (dependentTask in currentTask.dependentTasks) { //使用给定的协程上下文调用指定的挂起块,挂起直到完成,并返回结果。 withContext(currentTask.coroutineDispatcher) { // 这句代码很重要,不然会有死锁,想一想为什么? jobMap[dependentTask]!!.join() // 依赖的任务必须先执行完,因为这个是拓扑排序执行的,所以理论上jobMap[dep]不可能为空,当然有可能填错任务名称的 } } //依赖已经执行完成,执行自身的任务 val startTime = System.currentTimeMillis() logger.info { ">>>>>> Task ${currentTask.name} start <<<<<<" } val intercept = currentTask.run() logger.info { "Task ${currentTask.name} notifyObservers $intercept" } val endTime = System.currentTimeMillis() val costTime = (endTime - startTime) totalCostTime += costTime logger.info { "<<<<<< Task ${currentTask.name} completed, cost $costTime ms >>>>>>" } //添加到任务完成集合 finishedTaskList.add(currentTask) //通知观察者 currentTask.notify(intercept) //更新节点耗时 plantUMLCreator.getPlantUMLActivityNode(currentTask.name)?.let { logger.info { "${currentTask.name} getPlantUMLActivityNode ${it.elements}" } logger.info { "${currentTask.name} costTime $costTime" } it.elements[currentTask.name]?.costTime = costTime } //如果拦截,则取消剩余未完成的job if (intercept) { project.release() queue.clear() for ((taskName, job) in jobMap) { if (job.isActive) { job.cancel() logger.info { "Task $taskName $job cancel" } } } jobMap.clear() logger.warn { "============ Task ${currentTask.name} intercept, clear queue and cancel all jobs " } } logger.info { "${currentTask.name} taskList size ${taskList.size} finishedTaskList size ${finishedTaskList.size} " } if (taskList.size == finishedTaskList.size) { logger.info { "All tasks completed cost $totalCostTime ms " } //保存 logger.info { "All tasks completed cost $totalCostTime ms " } plantUMLCreator.create() } } //当前任务已经开始分配,找到所有依赖当前任务的任务,将它们的入度减1,如果入度为0,则加入队列 val toBeOfferTasks = mutableListOf() //记录当前任务是否有被其它任务依赖 var currentTaskHasDependentTasks = false for (task in taskList) { if (task.dependentTasks.contains(currentTask.name)) { currentTaskHasDependentTasks = true //入度减1 task.inDegree-- //入度为0,则进入队列 if (task.inDegree == 0) { toBeOfferTasks.add(task) } } } logger.info { "${currentTask.name} currentTaskHasDependentTasks $currentTaskHasDependentTasks toBeOfferTasks $toBeOfferTasks" } //修改任务end标记 plantUMLCreator.getPlantUMLActivityNode(currentTask.name)?.apply { elements[currentTask.name]?.end = !currentTaskHasDependentTasks } //添加下一个活动节点 if (toBeOfferTasks.isNotEmpty()) { if (toBeOfferTasks.size > 1) { val parallelTasks = mutableListOf() for (task in toBeOfferTasks) { parallelTasks.add(task.name) } logger.info { "${currentTask.name} addParallelTask $parallelTasks" } plantUMLCreator.addParallelTask(parallelTasks) } else { toBeOfferTasks[0].let { logger.info { "${currentTask.name} addSingleTask ${it.name}" } plantUMLCreator.addSingleTask(it.name) } } //将入度为零的任务加入队列 for (task in toBeOfferTasks) { queue.offer(task) } } } // 这个地方需要判断一下,是否所有的任务都已经被安排执行了,如果还有任务没有被安排,说明任务存在循环依赖,抛出异常。 if (taskList.isNotEmpty() && jobMap.isNotEmpty() && jobMap.size != taskList.size) { //循环结束后,如果有任务没有安排,则jobMap数量肯定是不等于taskList的数量 throw Throwable("Exist Recycle Task!") } logger.info { "All task assignments completed" } } }

PlantUMLCreator

PlantUMLCreator用于生成PlantUML Activity示意图。见:activity-diagram-beta

/*
 * 创建PlantUML 活动图(beta版)
 */
class PlantUMLCreator : XTaskLogger {

    companion object {
        private const val START_UML = "@startuml"
        private const val END_UML = "@enduml"
        private const val START = "start"
        private const val END = "end"
        private const val STOP = "stop"
        private const val FORK = "fork"
        private const val FORK_AGAIN = "fork again"
        private const val END_FORK = "end fork"
        private const val COLON = ":"
        private const val SEMI_COLON = ";"
        private const val LINE_BREAK = "\r\n"
    }

    /**
     * PlantUML内容
     */
    private var text: String = ""

    /**
     * 活动类,见https://plantuml.com/zh/activity-diagram-beta
     */
    private val activityDiagram = PlantUMLActivityDiagram()

    /**
     * 重置数据
     */
    fun reset() {
        activityDiagram.nodeDeque.clear()
    }

    private fun startUml() {
        text = START_UML + LINE_BREAK
    }

    private fun endUml() {
        text += END_UML + LINE_BREAK
    }

    private fun start() {
        text += START + LINE_BREAK
    }

    private fun stop() {
        text += STOP + LINE_BREAK
    }

    private fun end() {
        text += END + LINE_BREAK
    }

    private fun fork() {
        text += FORK + LINE_BREAK
    }

    private fun forkAgain() {
        text += FORK_AGAIN + LINE_BREAK
    }

    private fun endFork() {
        text += END_FORK + LINE_BREAK
    }

    private fun addActivityLabel(label: String) {
        text += COLON + label + SEMI_COLON + LINE_BREAK
    }

    /**
     * 根据任务名称获取指定的活动节点
     * @param taskName String
     * @return PlantUMLActivityNode?
     */
    fun getPlantUMLActivityNode(taskName: String): PlantUMLActivityNode? =
        activityDiagram.nodeMap[taskName]

    /**
     * 添加一个单线任务
     * @param taskName String
     */
    fun addSingleTask(taskName: String): PlantUMLActivityNode = PlantUMLActivityNode().apply {
        elements[taskName] = PlantUMLActivityNodeElement()
        activityDiagram.nodeDeque.add(this)
        activityDiagram.nodeMap[taskName] = this
        logger.info { "addSingleTask: $taskName $elements" }
    }

    /**
     * 添加并行任务
     * @param taskNameList List
     */
    fun addParallelTask(taskNameList: List): PlantUMLActivityNode =
        PlantUMLActivityNode().apply {
            logger.info { "addParallelTask: taskNameList $taskNameList" }
            for (taskName in taskNameList) {
                elements[taskName] = PlantUMLActivityNodeElement()
                activityDiagram.nodeMap[taskName] = this
            }
            activityDiagram.nodeDeque.add(this)
            logger.info { "addParallelTask: $taskNameList $elements" }
        }


    /**
     * 生成plantuml代码
     */
    fun create(): String {
        startUml()
        start()
        //logger.info { "create: node size is ${activityDiagram.nodeDeque.size}" }
        while (activityDiagram.nodeDeque.isNotEmpty()) {
            activityDiagram.nodeDeque.removeFirstOrNull()?.run {
                //logger.info { "create: node ${this.elements}" }
                if (elements.size == 1) {
                    for (ele in elements) {
                        logger.info { "create: element ${ele.key}" }
                        addActivityLabel("${ele.key} ${ele.value.costTime}ms")
                    }
                    return@run
                }
                var first = true
                for (ele in elements) {
                    if (first) {
                        first = false
                        fork()
                    } else {
                        forkAgain()
                    }
                    //logger.info { "create: element $ele" }
                    addActivityLabel("${ele.key} ${ele.value.costTime}ms")
                    if (ele.value.end) {
                        end()
                    }
                }
                endFork()
            }
        }
        stop()
        endUml()
        logger.info { "create: \n\n$text" }
        return text
    }

}


/**
 * 节点元素
 * @property end Boolean true没有被其它任务依赖
 * @property costTime Long 消耗时长(毫秒ms)
 * @constructor
 */
data class PlantUMLActivityNodeElement(var end: Boolean = false, var costTime: Long = 0L)

/**
 * PlantUML活动节点
 * @property elements MutableMap Key->Value:节点元素->结束标志
 */
class PlantUMLActivityNode {
    val elements = mutableMapOf()
}

/**
 * PlantUML活动示意图类
 * @property nodeDeque Deque 双端队列
 * @property nodeMap Deque map方便获取Node节点
 */
class PlantUMLActivityDiagram {
    val nodeDeque: ArrayDeque = ArrayDeque()
    val nodeMap = mutableMapOf()
}

线程调度

原本的线程调度是通过Task中的mainThread属性来判断是否需要在主线程中调用,这样有一个不好的地方就是不能指定任务的调度线程,也不方便做单元测试,改成可指定CoroutineDispatcher自由度会更高一些。

代码如下:

data class XTask(
    val name: String,//任务名称,不可以重复
    val desc: String = "",//任务描述
    val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,//线程调度
    val priority: Int = 0,   // 运行的优先级,值小的先执行
    var dependentTasks: Set = kotlin.collections.setOf(), // 依赖的任务集合(不可重复,所以用Set)
    val run: suspend () -> Boolean = { false }, //具体的执行任务的函数,为什么放在构造函数,主要是方便全局查看任务。返回是否拦截
) 

默认为Dispatchers.Default,在Android中如果需要在主线程运行,可以指定为Dispatchers.Main。

在本地单元测试中,封装 Android UI线程的Dispatchers.Main是无法使用的,因为我们执行的单元测试是在本地 JVM中,并不是在 Android 设备上。如果被测试代码使用了Dispatchers.Main,那么单元测试就会在运行过程中抛出异常。

注意:这种情况只会发生于本地单元测试。在可使用真实界面线程的 插桩测试 中,不应该替换 Main 调度程序。

如果需要在所有情况下都将 Dispatchers.Main调度程序替换为 TestDispatcher,则可以使用 Dispatchers.setMain 和 Dispatchers.resetMain 函数。

代码示例:

@Test
fun settingMainDispatcher() = runTest {
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    Dispatchers.setMain(testDispatcher)
    try {
        val viewModel = HomeViewModel()
        viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly
        assertEquals("Greetings!", viewModel.message.value)
    } finally {
        Dispatchers.resetMain()
    }
}

更多关于协程的单元测试内容参考:在 Android 上测试 Kotlin 协程

一个完整的单元测试用例如下:

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {

    @Test
    fun testXTask() = runTest(UnconfinedTestDispatcher()) {
        val project = XTaskProject("NormalProject")
        project.addTask(XTask(
            name = "TaskStart", coroutineDispatcher = Dispatchers.Default
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskA",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskStart")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskB",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskStart")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskC",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskStart")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskD",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskA", "TaskC")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskE",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskD")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskF",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskD")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskG",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskD")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskH",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskF", "TaskG")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskI",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskH")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskJ",
            coroutineDispatcher = Dispatchers.Default,
            dependentTasks = setOf("TaskI")
        ) {
            delay(1000)
            false
        }).addTask(XTask(
            name = "TaskK",
            coroutineDispatcher = Dispatchers.Default,
        ) {
            delay(4000)
            false
        })
        XTaskManager.start(project)
    }

}

需要的注意是,当我们在本地单元测试中执行协程时,最好配合runTest和UnconfinedTestDispatcher来使用,否则无法立即执行协程。

runTest(UnconfinedTestDispatcher()) {
    //launch
}

任务优先级

有一些任务虽然是并行的,但是我们仍然希望这些任务有一个优先级,因此笔者加了个priority来对任务进行排序,priority值小的先执行。

override fun getTasks(): List {
    // 根据优先级排序,值小的先执行
    return taskList.apply {
        clear()
        addAll(ArrayList(taskMap.values))
        sortBy { it.priority }
    }
}

注意,实际上优先级是对入度一样的任务才有效。并不是指所有任务的执行先后的优先级,也不是线程的优先级。

任务拦截

需求是多样化的,实际我们可能有这样的场景,比如网络不通或者不需要进行数据同步时,之后的任务链就不用执行了,因此需要增加拦截机制,提前结束后续的任务链。

因为所有的任务都是通过协程执行并返回job的对象,因此,想要进行拦截,就可以通过执行job.cancel来完成。当然,我们还需一个触发条件,笔者是通过任务返回值来判断是否需要进行拦截的。

代码如下:

val run: suspend () -> Boolean = { false }, //具体的执行任务的函数,为什么放在构造函数,主要是方便全局查看任务。返回是否拦截

val intercept = currentTask.run()
...省略若干代码....
//如果拦截,则取消剩余未完成的job
if (intercept) {
    project.release()
    queue.clear()
    for ((taskName, job) in jobMap) {
        if (job.isActive) {
            job.cancel()
            logger.info { "Task $taskName $job cancel" }
        }
    }
    jobMap.clear()
    logger.warn { "============ Task ${currentTask.name} intercept, clear queue and cancel all jobs " }
}

但是,如果协程里面又运行了协程怎么办?套娃呢。嗯,这个确实不好处理。因此,笔者强烈不建议这样写代码,我们应该通过拆分代码,定义新的任务和依赖关系来实现我们的需求。

可能还会有这样的需求,比如TaskB依赖TaskA,但是TaskB是否执行依赖于TaskA的结果,其它的Task则不受干扰。

面对这样的需求,可能就需要任务之间进行数据传递,就像RxJava那样,TaskA把结果给到TaskB,TaskB发现不满足条件就不执行对应的代码。或者简单点,Task增加一个是否可运行的标记位,当TaskA执行完后根据需要去设置TaskB的标记位,TaskB判断标记位不满足则不执行对应的代码。

总之如果要按上面的需求做一个精细化的控制,还是有蛮多工作要做的,这样会让整个逻辑变得更加复杂,调试起来会难上加难。实际上,笔者认为更简单更合理的方式,应该是将需求拆分,分成不同的Project来处理。如果Project之间有依赖关系,比如有ProjectA和ProjectB,ProjectB必须在ProjectA的TaskA之后才能开始,那么我们就可以给Task设计一个任务完成通知或者说增加一个锚点,这样ProjectB就可以在收到TaskA任务完成通知或到达锚点后开始执行。

任务完成通知

任务完成通知可以使用观察者模式,但如果需求简单,其实一个任务回调就基本满足需求了。

data class XTask(
    val name: String,//任务名称,不可以重复
    val desc: String = "",//任务描述
    val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,//线程调度
    val priority: Int = 0,   // 运行的优先级,值小的先执行,
    var dependentTasks: Set = setOf(), // 依赖的任务集合(不可重复,所以用Set)
    val run: suspend () -> Boolean = { false }, //具体的执行任务的函数,为什么放在构造函数,主要是方便全局查看任务。返回是否拦截
) : Observable() {
    // 入度,用于有向无环图的拓扑排序
    var inDegree = dependentTasks.size

    /**
     * 通知通知观察者
     */
    fun notify(arg: Any) {
        setChanged()
        notifyObservers(arg)
    }
}
//添加观察者
val taskA = XTask(
    name = "TaskA",
    coroutineDispatcher = Dispatchers.Default,
) {
    delay(1000)
    false
}.apply {
    addObserver { _, arg ->
        println("$name update intercept $arg")
    }
}

//依赖已经执行完成,执行自身的任务
val intercept = currentTask.run()
...省略若干代码
//通知观察者
currentTask.notify(intercept)

使用Kotlin DSL进行任务编排

原本的写法,任务的创建和任务之间依赖关系的定义是捆绑在一起的,这样有个不好的地方就是,任务之间的依赖关系不太直观,而且如果要改依赖关系也不太方便。实际上,我们还可以提供新的任务编排方式,将任务的创建和任务之间的依赖关系分开。

首先创建任务并添加进Project:

val diagramProject = XTaskProject("DiagramProject")
val taskStart = XTask(
    name = "TaskStart", coroutineDispatcher = Dispatchers.Default
) {
    delay(1000)
    false
}
val taskA = XTask(
    name = "TaskA",
    coroutineDispatcher = Dispatchers.Default,
) {
    delay(1000)
    false
}
val taskB = XTask(
    name = "TaskB",
    coroutineDispatcher = Dispatchers.Default,
) {
    delay(1000)
    false
}
val taskC = XTask(
    name = "TaskC",
    coroutineDispatcher = Dispatchers.Default,
) {
    delay(1000)
    false
}
val taskEnd = XTask(
    name = "TaskEnd", coroutineDispatcher = Dispatchers.Default
) {
    delay(1000)
    false
}
//添加进Project
diagramProject.apply {
    addTask(taskStart)
    addTask(taskA)
    addTask(taskB)
    addTask(taskC)
    addTask(taskEnd)
}

使用Kotlin DSL任务编排:

/**
 * <- 表示左边的任务依赖于右边的任务,即做的任务必须等右边的任务执行完毕之后才能执行
 * taskEnd <- taskB <- taskA <-taskStar
 * taskEnd <- taskC <- taskA
 */
taskEnd.depends {
    on(
        taskB.depends {
            on(
                taskA.depends {
                    on(taskStart)
                }
            )
        },
        taskC.depends {
            on(taskA)
        },
    )
}

使用Kotlin中缀函数美化一下代码:


taskEnd depends on(
    taskB depends on(
        taskA depends on(
            taskStart
        )
    ),
    taskC depends on(taskA)
)

奇奇怪怪的代码,不过倒是可以看得清楚任务之间的依赖关系。

执行下任务并看下结果:

Android代码重构系列-02-使用Kotlin协程实现一个支持任务编排的轻量级启动器_第2张图片

任务的执行的先后顺序以及耗时一目了然,改成TaskEnd不依赖TaskC:

Android代码重构系列-02-使用Kotlin协程实现一个支持任务编排的轻量级启动器_第3张图片

可以看到TaskC没有链接到TaskEnd,说明TaskEnd不依赖TaskC。

任务执行结果可视化

可能有人会好奇上面的图是怎么生成的,手动用画图软件画的?当然不是!利用plantuml可以帮助我们通过代码来画图。笔者使用的是plantuml的活动图(新语法)来表示任务执行的耗时和任务之间的依赖关系,当然,如果大家有更好的建议,非常欢迎一起交流。

活动图(新语法)的语法就不多说了,不了解的小伙伴点上面的链接先学习下。

如下图所示,通过一段代码可以生成对应的图。

Android代码重构系列-02-使用Kotlin协程实现一个支持任务编排的轻量级启动器_第4张图片

这样一来,我们只需要关心怎么生成这段表示图的代码就可以了,不过前提是,你得了解它的语法吧。

代码如下:


/*
 * 创建PlantUML 活动图(beta版)
 */
class XTaskPlantUMLCreator : XTaskLogger {

    companion object {
        private const val START_UML = "@startuml"
        private const val END_UML = "@enduml"
        private const val START = "start"
        private const val END = "end"
        private const val STOP = "stop"
        private const val FORK = "fork"
        private const val FORK_AGAIN = "fork again"
        private const val END_FORK = "end fork"
        private const val COLON = ":"
        private const val SEMI_COLON = ";"
        private const val LINE_BREAK = "\r\n"
    }

    /**
     * PlantUML内容
     */
    private var text: String = ""

    /**
     * 活动类,见https://plantuml.com/zh/activity-diagram-beta
     */
    private val activityDiagram = PlantUMLActivityDiagram()

    /**
     * 重置数据
     */
    fun reset() {
        activityDiagram.nodeDeque.clear()
    }

    private fun startUml() {
        text = START_UML + LINE_BREAK
    }

    private fun endUml() {
        text += END_UML + LINE_BREAK
    }

    private fun start() {
        text += START + LINE_BREAK
    }

    private fun stop() {
        text += STOP + LINE_BREAK
    }

    private fun end() {
        text += END + LINE_BREAK
    }

    private fun fork() {
        text += FORK + LINE_BREAK
    }

    private fun forkAgain() {
        text += FORK_AGAIN + LINE_BREAK
    }

    private fun endFork() {
        text += END_FORK + LINE_BREAK
    }

    private fun addActivityLabel(label: String) {
        text += COLON + label + SEMI_COLON + LINE_BREAK
    }

    /**
     * 根据任务名称获取指定的活动节点
     * @param taskName String
     * @return PlantUMLActivityNode?
     */
    fun getPlantUMLActivityNode(taskName: String): PlantUMLActivityNode? =
        activityDiagram.nodeMap[taskName]

    /**
     * 添加一个单线任务
     * @param taskName String
     */
    fun addSingleTask(taskName: String): PlantUMLActivityNode = PlantUMLActivityNode().apply {
        elements[taskName] = PlantUMLActivityNodeElement()
        activityDiagram.nodeDeque.add(this)
        activityDiagram.nodeMap[taskName] = this
        logger.info { "addSingleTask: $taskName $elements" }
    }

    /**
     * 添加并行任务
     * @param taskNameList List
     */
    fun addParallelTask(taskNameList: List): PlantUMLActivityNode =
        PlantUMLActivityNode().apply {
            logger.info { "addParallelTask: taskNameList $taskNameList" }
            for (taskName in taskNameList) {
                elements[taskName] = PlantUMLActivityNodeElement()
                activityDiagram.nodeMap[taskName] = this
            }
            activityDiagram.nodeDeque.add(this)
            logger.info { "addParallelTask: $taskNameList $elements" }
        }


    /**
     * 生成plantuml代码
     */
    fun create(): String {
        startUml()
        start()
        //logger.info { "create: node size is ${activityDiagram.nodeDeque.size}" }
        while (activityDiagram.nodeDeque.isNotEmpty()) {
            activityDiagram.nodeDeque.removeFirstOrNull()?.run {
                //logger.info { "create: node ${this.elements}" }
                if (elements.size == 1) {
                    for (ele in elements) {
                        logger.info { "create: element ${ele.key}" }
                        addActivityLabel("${ele.key} ${ele.value.costTime}ms")
                    }
                    return@run
                }
                var first = true
                for (ele in elements) {
                    if (first) {
                        first = false
                        fork()
                    } else {
                        forkAgain()
                    }
                    //logger.info { "create: element $ele" }
                    addActivityLabel("${ele.key} ${ele.value.costTime}ms")
                    if (ele.value.end) {
                        end()
                    }
                }
                endFork()
            }
        }
        stop()
        endUml()
        logger.info { "create: \n\n$text" }
        return text
    }

}


/**
 * 节点元素
 * @property end Boolean true没有被其它任务依赖
 * @property costTime Long 消耗时长(毫秒ms)
 * @constructor
 */
data class PlantUMLActivityNodeElement(var end: Boolean = false, var costTime: Long = 0L)

/**
 * PlantUML活动节点
 * @property elements MutableMap Key->Value:节点元素->结束标志
 */
class PlantUMLActivityNode {
    val elements = mutableMapOf()
}

/**
 * PlantUML活动示意图类
 * @property nodeDeque Deque 双端队列
 * @property nodeMap Deque map方便获取Node节点
 */
class PlantUMLActivityDiagram {
    val nodeDeque: ArrayDeque = ArrayDeque()
    val nodeMap = mutableMapOf()
}

也没什么技巧,直接拼接就完事了。有了这段代码,我们就可以在任务执行的过程中,根据依赖关系,执行耗时,调用相关的方法来填充需要的信息。

需要注意的,最终我们只生成了需要的代码,如果要转成图片,需要通过plantuml网站或者下载它们提供的jar包来操作。参考:plantuml入门指南。

Android代码重构系列-02-使用Kotlin协程实现一个支持任务编排的轻量级启动器_第5张图片

小结

经过一番改造,一个使用Kotlin协程实现支持任务编排的轻量级启动器就完成了。虽然代码量小,但是正因为足够简单,所以容易上手方便调试,应对一般的需求足够了。正所谓,麻雀虽小五脏俱全。

正如前言所说,这个启动器的目的不是用来做Android应用的启动优化,因为启动优化实际上会涉及到很多知识点,需要涉及到的细节也更多。比如多进程,跨模块,线程收敛,线程监控等等。

当然,这个启动器还有一些细节需要处理,比如怎样防止依赖一个不存在的任务,怎么避免死锁,怎样处理异常等。这些问题留给大家去思考了。

写在最后,首先非常感谢您耐心阅读完整篇文章,坚持写原创且基于实战的文章不是件容易的事,如果本文刚好对您有点帮助,欢迎您给文章点赞评论,您的鼓励是笔者坚持不懈的动力。写博客不仅仅是巩固学习的一个好方式,更是一个观点碰撞查漏补缺的绝佳机会,若文章有不对之处非常欢迎指正,再次感谢。

参考资料

我爱田Hebe应用程序启动优化新思路 - Kotlin协程

czy1121的init

程序员徐公的Android 启动优化(一) - 有向无环图

网易云音乐技术团队的心遇 Android 启动优化实践:将启动时间降低 50%

你可能感兴趣的:(Android开发实践,kotlin)