Android コード リファクタリング シリーズ-02 - Kotlin コルーチンを使用してタスク スケジューリングをサポートする軽量ランチャーを実装する

序文

この記事のテーマはランチャーですが、著者はランチャーを最適化する方法や完璧なランチャーを実装する方法について書くつもりはありません。オープンソースのサードパーティ Android ランチャーには、Alibaba のalpha 、 alpha を参照して詳細の一部を改良したAnchors 、比較的多数の Starts を備えたandroid-startup、Android の公式app-startup など、すでに多くの優れたホイールが存在します。など待ってください。

この記事は、アプリケーション起動の最適化に関する私の Ai Tian Hebe新しいアイデアである Kotlin コルーチンに触発されており、よりスムーズに使用できるように、上記のオープンソース ライブラリを参照していくつかの変更を加えました。この記事では主に、この考えに基づく著者の変革のいくつかのポイントを共有しますが、読み続ける前に、インスピレーションの源からこの記事を読むことを強くお勧めします。

コード全体

まず、暫定的な理解を得るために、変更されたコードを見てください。

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<String> = setOf(), // 依赖的任务集合(不可重复,所以用Set)
    val run: suspend () -> Boolean = { false }, //具体的执行任务的函数,为什么放在构造函数,主要是方便全局查看任务。返回是否拦截
) : Observable() {
    // 入度,用于有向无环图的拓扑排序
    var inDegree = dependentTasks.size

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

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

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

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

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

XTaskプロジェクト

Anchors に新しく追加されたプロジェクトの定義を参照して、プロジェクトの考え方に応じてタスクチェーンをより柔軟にカスタマイズできます。

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

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

    /**
     * 为什么用List,因为需要对Task进行排序,可以说是空间换时间
     */
    private val taskList: MutableList<XTask> = 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<XTask> {
        // 根据优先级排序,值小的先执行
        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<XTask>

    fun release()

}

XTaskStarter

タスク ランチャーは主に PlantUML アクティビティ図の生成をサポートします。

package com.lonbon.lonbonxtask.core

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

/*
 * *****************************************************************************
 * <p>
 * Copyright (C),2007-2016, LonBon Technologies Co. Ltd. All Rights Reserved.
 * <p>
 * *****************************************************************************
 * 任务启动器
 */
object XTaskStarter : XTaskLogger {

    /**
     * 开始执行project中的任务
     * runBlocking 会阻塞线程,如果是在主线程调用,则必须小心ANR
     * @param project IXTaskProject
     */
    @JvmStatic
    fun start(project: IXTaskProject) = runBlocking {
        //获取按优先级排序的列表
        val taskList: List<XTask> = project.getTasks()
        //缓存完成的任务
        val finishedTaskList: MutableList<XTask> = mutableListOf()
        // 找出所有入度为0的任务,加入到队列当中
        val queue: Queue<XTask> = LinkedList()
        taskList.filter { it.inDegree == 0 }.forEach(queue::add)
        //建立一个map, 通过name可以获取协程的Job
        val jobMap = mutableMapOf<String, Job>()
        //循环执行队列里的任务
        logger.info { "Start assigning tasks, current time ${System.currentTimeMillis()}" }
        var totalCostTime = 0L
        //添加第一个活动节点
        val plantUMLCreator = PlantUMLCreator()
        plantUMLCreator.reset()
        if (queue.size > 1) {
            val parallelTasks = mutableListOf<String>()
            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<XTask>()
            //记录当前任务是否有被其它任务依赖
            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<String>()
                    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

PlantUML アクティビティ図を生成するには、PlantUMLCreator を使用します。参照:アクティビティ図-ベータ

/*
 * 创建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<String>
     */
    fun addParallelTask(taskNameList: List<String>): 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<PlantUMLActivityNodeElement, Boolean> Key->Value:节点元素->结束标志
 */
class PlantUMLActivityNode {
    val elements = mutableMapOf<String, PlantUMLActivityNodeElement>()
}

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

スレッドのスケジュール設定

本来のスレッドスケジューリングは、メインスレッドで呼び出す必要があるかどうかをTaskのmainThread属性で判断するものですが、タスクのスケジューリングスレッドを指定できず、単体テストを行うのが不便であるというデメリットがあります。変更することでCoroutineDispatcherの自由度が高く指定できます。

コードは以下のように表示されます。

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

デフォルトは Dispatchers.Default です。Android では、メインスレッドで実行する必要がある場合は、Dispatchers.Main を指定できます。

ローカル単体テストでは、Android UI スレッドをカプセル化する Dispatchers.Main は使用できません。これは、実行する単体テストが Android デバイス上ではなくローカル JVM 内で行われるためです。テスト対象のコードが Dispatchers.Main を使用している場合、単体テストは実行中に例外をスローします。

: これはローカル単体テストでのみ発生します。実際の UI スレッドが使用できる インストルメンテーション テスト では、メイン スケジューラを置き換えないでください

あらゆる場合に 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
}

タスクの優先順位

一部のタスクは並列ですが、これらのタスクには優先順位があることが望ましいため、作成者はタスクを並べ替えるために優先順位を追加し、優先順位の値は最初に実行するために小さい値にします。

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

優先度は実際には同じ次数のタスクに対してのみ有効であることに注意してください。すべてのタスクの実行の優先順位やスレッドの優先順位を指すものではありません。

タスクインターセプト

要件は多岐にわたりますが、実際には、ネットワークに到達できない場合やデータの同期が必要ない場合には、後続のタスクチェーンを実行する必要がない場合など、インターセプト機構を追加する必要がある場合もあります。後続のタスクチェーンを事前に終了してください。

すべてのタスクはコルーチンを通じて実行され、ジョブ オブジェクトを返すため、インターセプトしたい場合は、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 がフラグを満たしていないと判断した場合、対応するコードは実行されません。

つまり、上記の要件に従ってきめ細かい制御を行おうとすると、やるべきことがまだたくさんあり、ロジック全体がより複雑になり、デバッグがさらに困難になります。実際、著者は、要件を異なるプロジェクトに分割して処理する方が簡単で合理的な方法であるべきだと考えています。ProjectA と ProjectB など、プロジェクト間に依存関係がある場合、ProjectB は ProjectA の TaskA の後に開始する必要があります。その場合、ProjectB が TaskA を受信できるように、タスク完了通知を設計するか、タスクのアンカー ポイントを追加できます。タスクの完了後に実行が開始されます。通知が届くか、アンカーポイントに到達します。

タスク完了通知

タスクの完了通知にはオブザーバーモードも利用できますが、要件が単純であれば基本的にタスクのコールバックで対応できます。

data class XTask(
    val name: String,//任务名称,不可以重复
    val desc: String = "",//任务描述
    val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,//线程调度
    val priority: Int = 0,   // 运行的优先级,值小的先执行,
    var dependentTasks: Set<String> = 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 を使用したタスク オーケストレーション

本来の書き方では、タスクの作成とタスク間の依存関係の定義がひとまとめになっているため、タスク間の依存関係が直感的ではなく、依存関係の変更が不便であるという欠点があります。実際、タスクの作成をタスク間の依存関係から分離する、タスク オーケストレーションの新しい方法を提供することもできます。

まずタスクを作成し、プロジェクトに追加します。

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 infix 関数を使用してコードを美しくします。


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

奇妙なコードですが、タスク間の依存関係がはっきりとわかります。

次のタスクを実行し、結果を確認します。

タスクの実行順序と時間が一目でわかる TaskEnd への変更は TaskC に依存しません。

TaskC が TaskEnd にリンクされていないことがわかり、TaskEnd が TaskC に依存していないことがわかります。

タスクの実行結果の可視化

お絵描きソフトを使って手作業で描かれた上の絵がどのようにして生成されたのか気になる人もいるかもしれません。もちろん違います!plantumlを使用すると、コードを通じて絵を描くことができます。著者は、plantumlアクティビティ図 (新しい構文)を使用して、時間のかかるタスクの実行とタスク間の依存関係を表現しています。もちろん、より良い提案がある場合は、大歓迎です。

アクティビティ図の文法(新文法)については多くは言いませんが、分からない場合は上のリンクをクリックして学習してください。

以下の図に示すように、対応する図はコード 1 つで生成できます。

このように、この図のコードを生成する方法だけを気にする必要がありますが、その前提として、その構文を理解する必要があります。

コードは以下のように表示されます。


/*
 * 创建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<String>
     */
    fun addParallelTask(taskNameList: List<String>): 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<PlantUMLActivityNodeElement, Boolean> Key->Value:节点元素->结束标志
 */
class PlantUMLActivityNode {
    val elements = mutableMapOf<String, PlantUMLActivityNodeElement>()
}

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

スキルはなく、ただ繋ぎ合わせるだけで終わりです。このコードを使用すると、関連メソッドを呼び出して、タスク実行時の依存関係と実行時間に応じて必要な情報を入力できます。

最終的には必要なコードのみを生成したことに注意してください。これを画像に変換したい場合は、plantuml Web サイトにアクセスするか、そこから提供されているjarパッケージをダウンロードする必要があります。参照: Plantuml スタートガイド

まとめ

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

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

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

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

参考资料

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

czy1121init

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

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

おすすめ

転載: blog.csdn.net/xiangang12202/article/details/129300394