Android 启动优化-有向无环图

这几篇文章从 0 到 1,讲解 DAG 有向无环图是怎么实现的,以及在 Android 启动优化的应用。

推荐理由:现在挺多文章一谈到启动优化,动不动就聊拓扑结构,这篇文章从数据结构到算法、到设计都给大家说清楚了,开源项目也有非常强的借鉴意义。

前言

说到 Android 启动优化,大家第一时间可能会想到异步加载。将耗时任务放到子线程加载,等到所有加载任务加载完成之后,再进入首页。

多线程异步加载方案确实是 ok 的。但如果遇到前后依赖的关系呢。比如任务2 依赖于任务 1,这时候要怎么解决呢。

最简单的方案是将任务1 丢到主线程加载,然后再启动多线程异步加载。

如果遇到更复杂的依赖呢。

任务3 依赖于任务 2, 任务 2 依赖于任务 1 呢,这时候你要怎么解决。更复杂的依赖关系呢

总不能将任务 2,任务 3 都放到主线程加载吧,这样多线程加载的意义就不大了。

有没有更好的方案呢?

答案肯定是有的,使用有向无环图。它可以完美解决先后依赖关系。

重要概念

有向无环图(Directed Acyclic Graph, DAG)是有向图的一种,字面意思的理解就是图中没有环。常常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。

顶点:图中的一个点,比如顶点 1,顶点 2。

:连接两个顶点的线段叫做边,edge。

入度:代表当前有多少边指向它。

在上图中,顶掉 1 的入度是 0,因为没有任何边指向它。 顶掉 2 的入度是 1, 因为 顶掉 1 指向 顶掉 2. 同理可得出 5 的入度是 2,因为顶掉 4 和顶点 3 指向它

拓扑排序:拓扑排序是对一个有向图构造拓扑序列的过程。它具有如下特点。

  • 每个顶点出现且只出现一次。
  • 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面

由于有这个特点,因此常常用有向无环图的数据结构用来解决依赖关系。

上图中,拓扑排序之后,任务2肯定排在任务1之后,因为任务2依赖 任务1, 任务3肯定在任务2之后,因为任务3依赖任务2。

拓扑排序一般有两种算法,第一种是入度表法,第二种是 DFS 方法。下面,让我们一起来看一下怎么实现它。

入度表法

入度表法是根据顶点的入度来判断是否有依赖关系的。若顶点的入度不为 0,则表示它有前置依赖。它也常常被称作 BFS 算法

算法思想

  • 建立入度表,入度为 0 的节点先入队
  • 当队列不为空,进行循环判断
    • 节点出队,添加到结果 list 当中
    • 将该节点的邻居入度减 1
    • 若邻居节点入度为 0,加入队列
  • 若结果 list 与所有节点数量相等,则证明不存在环。否则,存在环

实例讲解

下图所示的有向无环图,采用入度表的方法获取拓扑排序过程。

!

首先,我们选择入度为 0 的顶点,这里顶点 1 的入度为 0,删除顶点 1 之后,图变成如下。

这时候,顶点 2 和顶点 4 的入度都为 0,我们可以随便删除一个顶点。(这也就是为什么图的拓扑排序不是唯一的原因)。这里我们删除顶点 2,图变成如下:

这时候,我们再删除顶点 4,图变成如下:

选择入度为 0 的顶点 3,删除顶点 3 之后,图标称如下,

最后剩余顶点5,输出顶点5,拓扑排序过程结束。最终的输出结果为:

到此,优先无环图的入度法的流程已经讲解完毕。你清楚了嘛。

代码的话,下期会一起给出。

时间复杂度

设 AOE 网有 n 个事件,e 个活动,则算法的主要执行是:

  • 求每个事件的ve值和vl值:时间复杂度是O(n+e) ;
  • 根据ve值和vl值找关键活动:时间复杂度是O(n+e) ;

因此,整个算法的时间复杂度是O(n+e)

DFS 算法

从上面的入度表法,我们可以知道,要得到有向无环图的拓扑排序,我们的关键点要找到入度为 0 的顶点。然后接着删除该结点的相邻所有边。再遍历所有结点。直到入度为 0 的队列为空。这种方法其实是 BFS。

说到 BFS,我们第一时间就想到 DFS。与 BFS 不同的是,DFS 的关键点在于找到,出度为0的顶点。

总结如下,深度优先搜索过程中,当到达出度为0的顶点时,需要进行回退。在执行回退时记录出度为0的顶点,将其入栈。则最终出栈顺序的逆序即为拓扑排序序列。

算法思想

  • 对图执行深度优先搜索。
  • 在执行深度优先搜索时,若某个顶点不能继续前进,即顶点的出度为0,则将此顶点入栈。
  • 最后得到栈中顺序的逆序即为拓扑排序顺序。

实例讲解

同样,以下图讲解 DFS 算法的过程。

(1) 从顶点 1 开始出发,开始执行深度优先搜索。顺序为1->2->3->5。

(2)深度优先搜索到达顶点5时,顶点5出度为0。将顶点5入栈。

(3)深度优先搜索执行回退,回退至顶点3。此时顶点3的出度为0,将顶点3入栈。

(4)回退至顶点2,顶点2出度为0,顶点2入栈。

(5)回退至顶点1,顶点1可以前进位置为顶点4,顺序为1->4。

(6)顶点4出度为0,顶点4入栈。

(7)回退至顶点1,顶点1出度为0,顶点1入栈。

(8)栈的逆序为1->4->2->3->5。此顺序为拓扑排序结果。

注意:

这里的栈画的并不准确,理论上应该是1在最上面,5在最下面,有人喜欢从下往上理解栈。

时间复杂度

时间复杂度分析:首先深度优先搜索的时间复杂度为O(V+E),而每次只需将完成访问的顶点存入数组中,需要O(1),因而总复杂度为O(V+E)。

小结

有向无环图的拓扑排序其实并不难,难度中等。通常,我们一般使用 BFS 算法来解决,DFS 算法比较少用

对于 BFS(入度表法),它的核心思想是

  1. 选择一个没有输入边(入度为0)的源顶点(若有多个则任选一个),
  2. 将它和它的输出边删除。重复源顶点的删除操作,直到不存在入度为0的源顶点为止。
  3. 最终,检测图中的顶点个数,若还有顶点存在则算法无解,否则顶点的删除顺序就是拓扑排序的输出顺序。

拓扑排序的原理以及解题思路

基本概念

拓扑排序的英文名是 Topological sorting。

拓扑排序要解决的问题是给一个图的所有节点排序。有向无环图才有拓扑排序,非有向无环图没有。

换句话说,拓扑排序必须满足以下条件

图必须是一个无环有向图。序列必须满足的条件:

  • 每个顶点出现且只出现一次。
  • 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。

实战

我们已 leetcode 上面的一道算法题目作为切入点进行讲解。

leeocode 210: leetcode-cn.com/problems/co…

eg: 现在你总共有 n 门课需要选,记为 0 到 n-1。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1

输入: 2, [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2

输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
     因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

这道题,很明显,看起来可以用有向无环图的解法来解决

BFS 算法

题目分析

我们首先引入有向图 描述依赖关系

示例:假设 n = 6,先决条件表:[ [3, 0], [3, 1], [4, 1], [4, 2], [5, 3], [5, 4] ]

  • 0, 1, 2 没有先修课,可以直接选。其余的,都要先修 2 门课
  • 我们用 有向图 描述这种 依赖关系 (做事的先后关系):

在有向图中,我们知道,有入度出度概念:

如果存在一条有向边 A --> B,则这条边给 A 增加了 1 个出度,给 B 增加了 1 个入度。所以顶点 0、1、2 的 入度为 0。 顶点 3、4、5 的 入度为 2

BFS 前准备工作

  • 我们关心 课程的入度 —— 该值要被减,要被监控
  • 我们关心 课程之间的依赖关系 —— 选这门课会减小哪些课的入度
  • 因此我们需要合适的数据结构,去存储这些关系,这个可以通过哈希表

解题思路

  • 维护一个 queue,里面都是入度为 0 的课程
  • 选择一门课,就让它出列,同时 查看哈希表,看它 对应哪些后续课
  • 将这些后续课的 入度 - 1,如果有减至 0 的,就将它推入 queue
  • 不再有新的入度 0 的课入列 时,此时 queue 为空,退出循环
    private  class Solution {
        public int[] findOrder(int num, int[][] prerequisites) {

            // 计算所有节点的入度,这里用数组代表哈希表,key 是 index, value 是 inDegree[index].实际开发当中,用 HashMap 比较灵活
            int[] inDegree = new int[num];
            for (int[] array : prerequisites) {
                inDegree[array[0]]++;
            }

            // 找出所有入度为 0 的点,加入到队列当中
            Queue<Integer> queue = new ArrayDeque<>();
            for (int i = 0; i < inDegree.length; i++) {
                if (inDegree[i] == 0) {
                    queue.add(i);
                }
            }
            
            ArrayList<Integer> result = new ArrayList<>();
            while (!queue.isEmpty()) {
                Integer key = queue.poll();
                result.add(key);
                // 遍历所有课程
                for (int[] p : prerequisites) {
                    // 改课程依赖于当前课程 key
                    if (key == p[1]) {
                        // 入度减一
                        inDegree[p[0]]--;
                        if (inDegree[p[0]] == 0) {
                            queue.offer(p[0]); // 加入到队列当中
                        }
                    }
                }
            }

            // 数量不相等,说明存在环
            if (result.size() != num) {
                return new int[0];
            }

            int[] array = new int[num];
            int index = 0;
            for (int i : result) {
                array[index++] = i;

            }

            return array;
        }
    }

DFS 解法

算法思想

  • 对图执行深度优先搜索。
  • 在执行深度优先搜索时,若某个顶点不能继续前进,即顶点的出度为0,则将此顶点入栈。
  • 最后得到栈中顺序的逆序即为拓扑排序顺序。
// 方法 2:邻接矩阵 + DFS   由于用的数组,每次都要遍历,效率比较低
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        if (numCourses == 0) return new int[0];
        // 建立邻接矩阵
        int[][] graph = new int[numCourses][numCourses];
        for (int[] p : prerequisites) {
            graph[p[1]][p[0]] = 1;
        }
        // 记录访问状态的数组,访问过了标记 -1,正在访问标记 1,还未访问标记 0
        int[] status = new int[numCourses];
        Stack<Integer> stack = new Stack<>();  // 用栈保存访问序列
        for (int i = 0; i < numCourses; i++) {
            if (!dfs(graph, status, i, stack)) return new int[0]; // 只要存在环就返回
        }
        int[] res = new int[numCourses];
        for (int i = 0; i < numCourses; i++) {
            res[i] = stack.pop();
        }
        return res;
    }

    private boolean dfs(int[][] graph, int[] status, int i, Stack<Integer> stack) {
        if (status[i] == 1) return false; // 当前节点在此次 dfs 中正在访问,说明存在环
        if (status[i] == -1) return true;

        status[i] = 1;
        for (int j = 0; j < graph.length; j++) {
            // dfs 访问当前课程的后续课程,看是否存在环
            if (graph[i][j] == 1 && !dfs(graph, status, j, stack)) return false;
        }
        status[i] = -1;  // 标记为已访问
        stack.push(i);
        return true;
    }

总结

这篇博客从实战的角度出发,介绍了有向无环图的两种解法,入度表法和 DFS 法。其中,入度表法很重要,必须掌握。下一篇,我们将从 项目实战的角度来讲解,怎样搭建一个有向无环图的通用框架,敬请期待。


AnchorTask 使用说明

简介

Android 启动优化,大家第一时间可能会想到异步加载。将耗时任务放到子线程加载,等到所有加载任务加载完成之后,再进入首页。

多线程异步加载方案确实是 ok 的。但如果遇到前后依赖的关系呢。比如任务2 依赖于任务 1,这时候要怎么解决呢。

这时候就可以使用 AnchorTask 解决,它的实现原理是构建一个有向无环图,拓扑排序之后,如果任务 B 依赖任务 A,那么 A 一定排在任务 B 之后。

基本使用

第一步:在 moulde build.gradle 配置远程依赖

implementation 'com.xj.android:anchortask:0.1.0'

第二步:自定义 AnchorTaskB,继承 AnchorTask,重写相应的方法

class AnchorTaskB : AnchorTask() {
    override fun isRunOnMainThread(): Boolean {
        return false
    }

    override fun run() {
        val start = System.currentTimeMillis()
        try {
            // 在这里进行操作,这里通过睡眠模拟耗时操作
            Thread.sleep(300)
        } catch (e: Exception) {
        }
        com.xj.anchortask.library.log.LogUtils.i(
            TAG, "AnchorTaskOne: " + (System.currentTimeMillis() - start)
        )
    }

    // 返回依赖的任务,这里是通过 class name 去找到对应的 task
    override fun getDependsTaskList(): List<Class<out AnchorTask>>? {
        return ArrayList<Class<out AnchorTask>>().apply {
            add(AnchorTaskA::class.java)
        }
    }

}

如果任务 C 依赖任务 B,任务 A,可以这样写

class AnchorTaskC : AnchorTask() {
   
    override fun getDependsTaskList(): List<Class<out AnchorTask>>? {
        return ArrayList<Class<out AnchorTask>>().apply {
            add(AnchorTaskA::class.java)
            add(AnchorTaskB::class.java)
        }
    }

}

最后,通过 AnchorTaskDispatcher.instance .addTask(AnchorTaskFive()) 添加任务,并调用 start() 方法启动, await() 方法表示阻塞等待所有任务执行完毕。

AnchorTaskDispatcher.instance.setContext(this).setLogLevel(LogUtils.LogLevel.DEBUG).setTimeOutMillion(1000L).
            .addTask(AnchorTaskZero())
            .addTask(AnchorTaskOne())
            .addTask(AnchorTaskTwo())
            .addTask(AnchorTaskThree())
            .addTask(AnchorTaskFour())
            .addTask(AnchorTaskFive())
            .start()
            .await()

AnchorTaskDispatcher 介绍

  1. AnchorTaskDispatcher start 方法必须在主线程调用,子线程调用会抛出异常。
  2. setTimeOutMillion 方法是配合 await() 方法使用的,单独调用没有任何效果,表示 await 等待的超时时间
  3. await 阻塞当前线程,等待所有任务执行完毕之后,会自动往下走
  4. await() 方法必须在 start 方法之后调用
  5. setThreadPoolExecutor 设置 task 执行的线程池

AnchorTask 介绍

AnchorTask 实现了 IAnchorTask 接口,主要有几个方法

  • isRunOnMainThread(): Boolean 表示是否在主线程运行,默认值是 false
  • priority(): Int 方法 表示线程的优先级别,默认值是 Process.THREAD_PRIORITY_FOREGROUND
  • needWait() 表示当我们调用 AnchorTaskDispatcher await 时,是否需要等待,return true,表示需要等待改任务执行结束,AnchorTaskDispatcher await 方法才能继续往下执行。
  • fun getDependsTaskList(): List<Class<out AnchorTask>>? 方法返回前置任务依赖,默认值是返回 null.
  • fun run() 方法,表示任务执行的时候
interface IAnchorTask : IAnchorCallBack {

    /**
     * 是否在主线程执行
     */
    fun isRunOnMainThread(): Boolean

    /**
     * 任务优先级别
     */
    @IntRange(
        from = Process.THREAD_PRIORITY_FOREGROUND.toLong(),
        to = Process.THREAD_PRIORITY_LOWEST.toLong()
    )
    fun priority(): Int

    /**
     * 调用 await 方法,是否需要等待改任务执行完成
     * true 不需要
     * false 需要
     */
    fun needWait(): Boolean

    /**
     * 当前任务的前置任务,可以用来确定顶点的入度
     */
    fun getDependsTaskList(): List<Class<out AnchorTask>>?

    /**
     * 任务被执行的时候回调
     */
    fun run()

}
class AnchorTaskOne : AnchorTask() {
    override fun isRunOnMainThread(): Boolean {
        return false
    }

    override fun run() {
        val start = System.currentTimeMillis()
        try {
            Thread.sleep(300)
        } catch (e: Exception) {
        }
        LogUtils.i(
            TAG, "AnchorTaskOne: " + (System.currentTimeMillis() - start)
        )
    }

}

监听任务的回调

val anchorTask = AnchorTaskTwo()
        anchorTask.addCallback(object : IAnchorCallBack {
            override fun onAdd() {
                com.xj.anchortask.LogUtils.i(TAG, "onAdd: $anchorTask")
            }

            override fun onRemove() {
                com.xj.anchortask.LogUtils.i(TAG, "onRemove: $anchorTask")
            }

            override fun onStart() {
                com.xj.anchortask.LogUtils.i(TAG, "onStart:$anchorTask ")
            }

            override fun onFinish() {
                com.xj.anchortask.LogUtils.i(TAG, "onFinish:$anchorTask ")
            }

        })


手把手教你实现 AnchorTask

原理简介

AnchorTask,锚点任务,它的实现原理是构建一个有向无环图,拓扑排序之后,如果任务 B 依赖任务 A,那么 A 一定排在任务 B 之前。

了解原理之前,请必须先了解有向无环图和多线程的一些基本知识,不然,下文,你基本是看不懂的

一个共识

  • 前置任务:任务 3 依赖于任务 0,1,那么任务 3 的前置任务是任务 0, 1
  • 子任务:任务 0 执行完之后,任务 3 才能执行,那么称呼任务 3 为 任务 0 的子任务

如何构建一个有向无环图

这里我们采用 BFS 方法实现,算法思想大概是这样的

  • 建立入度表,入度为 0 的节点先入队
  • 当队列不为空,进行循环判断
    • 节点出队,添加到结果 list 当中
    • 将该节点的邻居入度减 1
    • 若邻居课程入度为 0,加入队列
  • 若结果 list 与所有节点数量相等,则证明不存在环。否则,存在环

多线程中,任务执行是随机的,那如何保证任务被依赖的任务先于任务执行呢?

这里要解决的主要有三个问题

  1. 首先我们要解决一个问题,当前任务有哪些前置任务,这个可以用 list 存储,代表它依赖的任务 list。当它所依赖的任务 list 没有执行完毕,当前任务需要等待。
  2. 当前任务执行完毕之后,所有依赖它的子任务需要感知到。我们可以用一个 map 来存储这种关系,key 是当前任务,value 是依赖于当前任务的集合(list)
  3. 多线程当中,等待和唤醒功能,有多种方式可以实现。wait、notify 机制,ReentrantLock Condition 机制,CountDownLatch 机制。这里我们选择 CountDownLatch 机制,因为 CountDownLatch 有点类似于计数器,特别适合这种场景。

具体实现

IAnchorTask

首先,我们定义一个 IAnchorTask 接口,主要有一个方法

  • isRunOnMainThread(): Boolean 表示是否在主线程运行,默认值是 false
  • priority(): Int 方法 表示线程的优先级别,默认值是 Process.THREAD_PRIORITY_FOREGROUND
  • needWait() 表示当我们调用 AnchorTaskDispatcher await 时,是否需要等待,return true,表示需要等待改任务执行结束,AnchorTaskDispatcher await 方法才能继续往下执行。
  • fun getDependsTaskList(): List<Class<out AnchorTask>>? 方法返回前置任务依赖,默认值是返回 null.
  • fun run() 方法,表示任务执行的时候
interface IAnchorTask : IAnchorCallBack {

    /**
     * 是否在主线程执行
     */
    fun isRunOnMainThread(): Boolean

    /**
     * 任务优先级别
     */
    @IntRange(
        from = Process.THREAD_PRIORITY_FOREGROUND.toLong(),
        to = Process.THREAD_PRIORITY_LOWEST.toLong()
    )
    fun priority(): Int

    /**
     * 调用 await 方法,是否需要等待改任务执行完成
     * true 不需要
     * false 需要
     */
    fun needWait(): Boolean

    /**
     * 当前任务的前置任务,可以用来确定顶点的入度
     */
    fun getDependsTaskList(): List<Class<out AnchorTask>>?

    /**
     * 任务被执行的时候回调
     */
    fun run()

}

它有一个实现类 AnchorTask,增加了 await 和 countdown 方法

  • await 方法,调用它,当前任务会等待
  • countdown() 方法,如果当前计数器值 > 0,会减一,否则,什么也不操作
abstract class AnchorTask : IAnchorTask {

    private val countDownLatch: CountDownLatch = CountDownLatch(getListSize())
    private fun getListSize() = getDependsTaskList()?.size ?: 0

    companion object {
        const val TAG = "AnchorTask"
    }

    /**
     * self call,await
     */
    fun await() {
        countDownLatch.await()
    }

    /**
     * parent call, countDown
     */
    fun countdown() {
        countDownLatch.countDown()
    }
}

排序实现

无环图的拓扑排序,这里采用的是 BFS 算法。具体的可以见 AnchorTaskUtils#getSortResult 方法,它有三个参数

  • list 存储所有的任务
  • taskMap: MutableMap<Class<out AnchorTask>, AnchorTask> = HashMap()存储所有的任务,key 是 Class,value 是 AnchorTask
  • taskChildMap: MutableMap<Class<out AnchorTask>, ArrayList<Class<out AnchorTask>>?> = HashMap(),储存当前任务的子任务, key 是当前任务的 class,value 是 AnchorTask 的 list

算法思想

  1. 首先找出所有入度为 0 的队列,用 queue 变量存储
  2. 当队列不为空,进行循环判断。
    • 从队列 pop 出,添加到结果队列
    • 遍历当前任务的子任务,通知他们的入度减一(其实是遍历 taskChildMap),如果入度为 0,添加到队列 queue 里面
  3. 当结果队列和 list size 不相等试,证明有环
    @JvmStatic
    fun getSortResult(
        list: MutableList<AnchorTask>, taskMap: MutableMap<Class<out AnchorTask>, AnchorTask>,
        taskChildMap: MutableMap<Class<out AnchorTask>, ArrayList<Class<out AnchorTask>>?>
    ): MutableList<AnchorTask> {
        val result = ArrayList<AnchorTask>()
        // 入度为 0 的队列
        val queue = ArrayDeque<AnchorTask>()
        val taskIntegerHashMap = HashMap<Class<out AnchorTask>, Int>()

        // 建立每个 task 的入度关系
        list.forEach { anchorTask: AnchorTask ->
            val clz = anchorTask.javaClass
            if (taskIntegerHashMap.containsKey(clz)) {
                throw AnchorTaskException("anchorTask is repeat, anchorTask is $anchorTask, list is $list")
            }

            val size = anchorTask.getDependsTaskList()?.size ?: 0
            taskIntegerHashMap[clz] = size
            taskMap[clz] = anchorTask
            if (size == 0) {
                queue.offer(anchorTask)
            }
        }

        // 建立每个 task 的 children 关系
        list.forEach { anchorTask: AnchorTask ->
            anchorTask.getDependsTaskList()?.forEach { clz: Class<out AnchorTask> ->
                var list = taskChildMap[clz]
                if (list == null) {
                    list = ArrayList<Class<out AnchorTask>>()
                }
                list.add(anchorTask.javaClass)
                taskChildMap[clz] = list
            }
        }

        // 使用 BFS 方法获得有向无环图的拓扑排序
        while (!queue.isEmpty()) {
            val anchorTask = queue.pop()
            result.add(anchorTask)
            val clz = anchorTask.javaClass
            taskChildMap[clz]?.forEach { // 遍历所有依赖这个顶点的顶点,移除该顶点之后,如果入度为 0,加入到改队列当中
                var result = taskIntegerHashMap[it] ?: 0
                result--
                if (result == 0) {
                    queue.offer(taskMap[it])
                }
                taskIntegerHashMap[it] = result
            }
        }

        // size 不相等,证明有环
        if (list.size != result.size) {
            throw AnchorTaskException("Ring appeared,Please check.list is $list, result is $result")
        }

        return result

    }

AnchorTaskDispatcher

AnchorTaskDispatcher 这个类很重要,有向无环图的拓扑排序和多线程的依赖唤醒,都是借助这个核心类完成的。

它主要有几个成员变量

// 存储所有的任务
    private val list: MutableList<AnchorTask> = ArrayList()

    // 存储所有的任务,key 是 Class<out AnchorTask>,value 是 AnchorTask
    private val taskMap: MutableMap<Class<out AnchorTask>, AnchorTask> = HashMap()

    // 储存当前任务的子任务, key 是当前任务的 class,value 是 AnchorTask 的 list
    private val taskChildMap: MutableMap<Class<out AnchorTask>, ArrayList<Class<out AnchorTask>>?> =
        HashMap()

    // 拓扑排序之后的主线程任务
    private val mainList: MutableList<AnchorTask> = ArrayList()

    // 拓扑排序之后的子线程任务
    private val threadList: MutableList<AnchorTask> = ArrayList()

    //需要等待的任务总数,用于阻塞
    private lateinit var countDownLatch: CountDownLatch

    //需要等待的任务总数,用于CountDownLatch
    private val needWaitCount: AtomicInteger = AtomicInteger()

它有一个比较重要的方法 setNotifyChildren(anchorTask: AnchorTask) ,有一个方法参数 AnchorTask,它的作用是通知该任务的子任务,当前任务执行完毕,入度数减一。

    /**
     *  通知 child countdown,当前的阻塞任务书也需要 countdown
     */
    fun setNotifyChildren(anchorTask: AnchorTask) {
        taskChildMap[anchorTask::class.java]?.forEach {
            taskMap[it]?.countdown()
        }
        if (anchorTask.needWait()) {
            countDownLatch.countDown()
        }
    }

接下来看一下 start 方法

fun start(): AnchorTaskDispatcher {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw AnchorTaskException("start method should be call on main thread")
        }
        startTime = System.currentTimeMillis()

        val sortResult = AnchorTaskUtils.getSortResult(list, taskMap, taskChildMap)
        LogUtils.i(TAG, "start: sortResult is $sortResult")
        sortResult.forEach {
            if (it.isRunOnMainThread()) {
                mainList.add(it)
            } else {
                threadList.add(it)
            }
        }

        countDownLatch = CountDownLatch(needWaitCount.get())

        val threadPoolExecutor =
            this.threadPoolExecutor ?: TaskExecutorManager.instance.cpuThreadPoolExecutor

        threadList.forEach {
            threadPoolExecutor.execute(AnchorTaskRunnable(this, anchorTask = it))
        }

        mainList.forEach {
            AnchorTaskRunnable(this, anchorTask = it).run()
        }

        return this
    }

它主要干几件事

  • 检测是否在主线程,不是抛出异常,这里为什么要检测在主线程呢?主要是构建有向无环图的过程,我们必须保证是线程安全的
  • 获取有向无环图的拓扑排序
  • 根据拓扑排序的排序结果,执行相应的任务。可以看到在执行任务的时候,我们使用 AnchorTaskRunnable 包裹起来
class AnchorTaskRunnable(
    private val anchorTaskDispatcher: AnchorTaskDispatcher,
    private val anchorTask: AnchorTask
) : Runnable {

    override fun run() {
        Process.setThreadPriority(anchorTask.priority())
        //  前置任务没有执行完毕的话,等待,执行完毕的话,往下走
        anchorTask.await()
        anchorTask.onStart()
        // 执行任务
        anchorTask.run()
        anchorTask.onFinish()
        // 通知子任务,当前任务执行完毕了,相应的计数器要减一。
        anchorTaskDispatcher.setNotifyChildren(anchorTask)
    }
}

AnchorTaskRunnable 有点类似于装饰者模式,多线程依赖的执行关系在这里都得到体现,只有几行代码

  1. 前置任务没有执行完毕的话,等待,执行完毕的话,往下走
  2. 执行任务
  3. 通知子任务,当前任务执行完毕了,相应的计数器(入度数)要减一。

总结

AnchorTask 的原理不复杂,本质是有向无环图与多线程知识的结合。

  1. 根据 BFS 构建出有向无环图,并得到它的拓扑排序
  2. 在多线程执行过程中,我们是通过任务的子任务关系和 CounDownLatch 确保先后执行关系的
    1. 前置任务没有执行完毕的话,等待,执行完毕的话,往下走
    2. 执行任务
    3. 通知子任务,当前任务执行完毕了,相应的计数器(入度数)要减一。

AnchorTask 源码已经更新到 github,AnchorTask。 

在实现这个开源框架的时候,借鉴了以下开源框架的思想。AppStartFaster 主要是通过 ClassName 找到相应的 Task,而阿里 alpha 是通过 taskName 找到相应的 Task,并且需要指定 ITaskCreator。两种方式各有优缺点,没有优劣之说,具体看使用场景。

android-startup

alpha

AppStartFaster


更新说明

  1. 之前的 0.1.0 版本 配置前置依赖任务,是通过 AnchorTask getDependsTaskList 的方式,他是通过 className 找到 AnchorTask,并且内聚在当前的 AnchorTask 中,从全局的角度看 ,这种方式不太直观,1.0.0 放弃了这种方式,参考阿里 Alpha 的方式,通过 addTask(TASK_NAME_THREE).afterTask(TASK_NAME_ZERO, TASK_NAME_ONE)
  2. 1.0.0 版本新增了 Project 类,并增加 OnProjectExecuteListener 监听
  3. 1.0.0 版本新增 OnGetMonitorRecordCallback 监听,方便统计各个任务的耗时

说明

Android 启动优化,大家第一时间可能会想到异步加载。将耗时任务放到子线程加载,等到所有加载任务加载完成之后,再进入首页。

多线程异步加载方案确实是 ok 的。但如果遇到前后依赖的关系呢。比如任务2 依赖于任务 1,这时候要怎么解决呢。

假设我们有这样的任务依赖

我们要怎么使用它呢

        val project =
            AnchorProject.Builder().setContext(context).setLogLevel(LogUtils.LogLevel.DEBUG)
                .setAnchorTaskCreator(ApplicationAnchorTaskCreator())
                .addTask(TASK_NAME_ZERO)
                .addTask(TASK_NAME_ONE)
                .addTask(TASK_NAME_TWO)
                .addTask(TASK_NAME_THREE).afterTask(TASK_NAME_ZERO, TASK_NAME_ONE)
                .addTask(TASK_NAME_FOUR).afterTask(TASK_NAME_ONE, TASK_NAME_TWO)
                .addTask(TASK_NAME_FIVE).afterTask(TASK_NAME_THREE, TASK_NAME_FOUR)
                .build()
        project.start().await()
class ApplicationAnchorTaskCreator : IAnchorTaskCreator {
    override fun createTask(taskName: String): AnchorTask? {
        when (taskName) {
            TASK_NAME_ZERO -> {
                return AnchorTaskZero()
            }

            TASK_NAME_ONE -> {
                return AnchorTaskOne()
            }
            TASK_NAME_TWO -> {
                return AnchorTaskTwo()
            }
            TASK_NAME_THREE -> {
                return AnchorTaskThree()
            }
            TASK_NAME_FOUR -> {
                return AnchorTaskFour()
            }
            TASK_NAME_FIVE -> {
                return AnchorTaskFive()
            }
        }
        return null
    }

}

Demo 跑起来,可以看到预期的效果。

基本使用

第一步:在 moulde build.gradle 配置远程依赖

implementation 'com.xj.android:anchortask:1.0.0'

最新的版本号可以看这里 lastedt version

第二步:自定义 AnchorTaskZero,继承 AnchorTask,并指定 taskName,注意 taskName 必须是唯一的,因为我们会根据 taskName 找到相应的 AnchorTask 重写相应的方法

class AnchorTaskZero() : AnchorTask(TASK_NAME_ZERO) {
    override fun isRunOnMainThread(): Boolean {
        return false
    }

    override fun run() {
        val start = System.currentTimeMillis()
        try {
            Thread.sleep(300)
        } catch (e: Exception) {
        }
        LogUtils.i(
            TAG, "AnchorTaskOne: " + (System.currentTimeMillis() - start)
        )
    }
}

如果任务 三 依赖任务 二,任务 一,可以这样写

addTask(TASK_NAME_THREE).afterTask(TASK_NAME_ZERO, TASK_NAME_ONE)

最后,通过 project.start() 方法启动, 如果需要阻塞等待,调用 await() 方法

AnchorProject.Builder().setContext(context).setLogLevel(LogUtils.LogLevel.DEBUG)
                .setAnchorTaskCreator(ApplicationAnchorTaskCreator())
                .addTask(TASK_NAME_ZERO)
                .addTask(TASK_NAME_ONE)
                .addTask(TASK_NAME_TWO)
                .addTask(TASK_NAME_THREE).afterTask(TASK_NAME_ZERO, TASK_NAME_ONE)
                .addTask(TASK_NAME_FOUR).afterTask(TASK_NAME_ONE, TASK_NAME_TWO)
                .addTask(TASK_NAME_FIVE).afterTask(TASK_NAME_THREE, TASK_NAME_FOUR)
                .build()
project.start().await()

监听任务回调

project.addListener(object : OnProjectExecuteListener {
             
            // project 开始执行的时候
            override fun onProjectStart() {
                com.xj.anchortask.LogUtils.i(MyApplication.TAG, "onProjectStart ")
            }

            // project 执行一个 task 完成的时候
            override fun onTaskFinish(taskName: String) {
                com.xj.anchortask.LogUtils.i(
                    MyApplication.TAG,
                    "onTaskFinish, taskName is $taskName"
                )
            }

            // project 执行完成的时候
            override fun onProjectFinish() {
                com.xj.anchortask.LogUtils.i(MyApplication.TAG, "onProjectFinish ")
            }

        })

添加每个任务执行耗时回调

project.onGetMonitorRecordCallback = object : OnGetMonitorRecordCallback {
           
            // 所有 task 执行完毕会调用这个方法,Map 存储了 task 的执行时间, key 是 taskName,value 是时间,单位毫秒
            override fun onGetTaskExecuteRecord(result: Map<String?, Long?>?) {
                onGetMonitorRecordCallback?.onGetTaskExecuteRecord(result)
            }
            
            // 所有 task 执行完毕会调用这个方法,costTime 执行时间
            override fun onGetProjectExecuteTime(costTime: Long) {
                onGetMonitorRecordCallback?.onGetProjectExecuteTime(costTime)
            }

        }

AnchorProject 介绍

  1. AnchorTaskDispatcher start 方法必须在主线程调用,子线程调用会抛出异常。
  2. await 阻塞当前线程,等待所有任务执行完毕之后,会自动往下走,await 方法携带一个参数,timeOutMillion 表示超时等待的时间
  3. await() 方法必须在 start 方法之后调用
  4. 添加任务是通过 AnchorProject.Builder().addTask 添加的,典型的构造模式
  5. 设置执行的线程池,可以通过 AnchorProject.Builder().setThreadPoolExecutor(TaskExecutorManager.instance.cpuThreadPoolExecutor)

AnchorTask 介绍

AnchorTask 实现了 IAnchorTask 接口,主要有几个方法

  • isRunOnMainThread(): Boolean表示是否在主线程运行,默认值是 false
  • priority(): Int 方法 表示线程的优先级别,默认值是 Process.THREAD_PRIORITY_FOREGROUND
  • needWait() 表示当我们调用 AnchorTaskDispatcher await 时,是否需要等待,return true,表示需要等待改任务执行结束,AnchorTaskDispatcher await 方法才能继续往下执行。
  • fun run() 方法,表示任务执行的时候
interface IAnchorTask : IAnchorCallBack {

    /**
     * 是否在主线程执行
     */
    fun isRunOnMainThread(): Boolean

    /**
     * 任务优先级别
     */
    @IntRange(
        from = Process.THREAD_PRIORITY_FOREGROUND.toLong(),
        to = Process.THREAD_PRIORITY_LOWEST.toLong()
    )
    fun priority(): Int

    /**
     * 调用 await 方法,是否需要等待改任务执行完成
     * true 不需要
     * false 需要
     */
    fun needWait(): Boolean

    /**
     * 任务被执行的时候回调
     */
    fun run()

}


abstract class AnchorTask(private val name: String) : IAnchorTask {

    companion object {
        const val TAG = "AnchorTask"
    }

    private lateinit var countDownLatch: CountDownLatch
    private val copyOnWriteArrayList: CopyOnWriteArrayList<IAnchorCallBack> by lazy {
        CopyOnWriteArrayList<IAnchorCallBack>()
    }

    val dependList: MutableList<String> = ArrayList()


    private fun getListSize() = getDependsTaskList()?.size ?: 0

    override fun getTaskName(): String {
        return name
    }

    override fun priority(): Int {
        return Process.THREAD_PRIORITY_FOREGROUND
    }

    override fun needWait(): Boolean {
        return true
    }

    fun afterTask(taskName: String) {
        dependList.add(taskName)
    }

    /**
     * self call,await
     */
    fun await() {
        tryToInitCountDown()
        countDownLatch.await()
    }

    @Synchronized
    private fun tryToInitCountDown() {
        if (!this::countDownLatch.isInitialized) {
            countDownLatch = CountDownLatch(dependList.size)
        }
    }

    /**
     * parent call, countDown
     */
    fun countdown() {
        tryToInitCountDown()
        countDownLatch.countDown()
    }

    override fun isRunOnMainThread(): Boolean {
        return false
    }

    fun getDependsTaskList(): List<String>? {
        return dependList
    }

    @CallSuper
    override fun onAdd() {
        copyOnWriteArrayList.forEach {
            it.onAdd()
        }
    }

    @CallSuper
    override fun onStart() {
        copyOnWriteArrayList.forEach {
            it.onStart()
        }
    }

    @CallSuper
    override fun onFinish() {
        copyOnWriteArrayList.forEach {
            it.onFinish()
        }
    }

    fun addCallback(iAnchorCallBack: IAnchorCallBack?) {
        iAnchorCallBack ?: return
        copyOnWriteArrayList.add(iAnchorCallBack)
    }

    fun removeCallback(iAnchorCallBack: IAnchorCallBack?) {
        iAnchorCallBack ?: return
        copyOnWriteArrayList.remove(iAnchorCallBack)
    }

    override fun toString(): String {
        return "AnchorTask(name='$name',dependList is $dependList)"
    }


}


class AnchorTaskOne : AnchorTask() {
    override fun isRunOnMainThread(): Boolean {
        return false
    }

    override fun run() {
        val start = System.currentTimeMillis()
        try {
            Thread.sleep(300)
        } catch (e: Exception) {
        }
        LogUtils.i(
            TAG, "AnchorTaskOne: " + (System.currentTimeMillis() - start)
        )
    }

}

监听任务的回调

val anchorTask = AnchorTaskTwo()
        anchorTask.addCallback(object : IAnchorCallBack {
            override fun onAdd() {
                com.xj.anchortask.LogUtils.i(TAG, "onAdd: $anchorTask")
            }

            override fun onStart() {
                com.xj.anchortask.LogUtils.i(TAG, "onStart:$anchorTask ")
            }

            override fun onFinish() {
                com.xj.anchortask.LogUtils.i(TAG, "onFinish:$anchorTask ")
            }

        })

总结

AnchorTask 的原理不复杂,本质是有向无环图与多线程知识的结合。

  1. 根据 BFS 构建出有向无环图,并得到它的拓扑排序
  2. 在多线程执行过程中,我们是通过任务的子任务关系和 CounDownLatch 确保先后执行关系的
    1. 前置任务没有执行完毕的话,等待,执行完毕的话,往下走
    2. 执行任务
    3. 通知子任务,当前任务执行完毕了,相应的计数器(入度数)要减一。

AnchorTask

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值