这几篇文章从 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(入度表法),它的核心思想是
- 选择一个没有输入边(入度为0)的源顶点(若有多个则任选一个),
- 将它和它的输出边删除。重复源顶点的删除操作,直到不存在入度为0的源顶点为止。
- 最终,检测图中的顶点个数,若还有顶点存在则算法无解,否则顶点的删除顺序就是拓扑排序的输出顺序。
拓扑排序的原理以及解题思路
基本概念
拓扑排序的英文名是 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 介绍
AnchorTaskDispatcher start
方法必须在主线程调用,子线程调用会抛出异常。setTimeOutMillion
方法是配合 await() 方法使用的,单独调用没有任何效果,表示 await 等待的超时时间await
阻塞当前线程,等待所有任务执行完毕之后,会自动往下走await()
方法必须在 start 方法之后调用setThreadPoolExecutor
设置 task 执行的线程池
AnchorTask 介绍
AnchorTask 实现了 IAnchorTask 接口,主要有几个方法
isRunOnMainThread(): Boolean
表示是否在主线程运行,默认值是 falsepriority(): Int
方法 表示线程的优先级别,默认值是 Process.THREAD_PRIORITY_FOREGROUNDneedWait()
表示当我们调用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 与所有节点数量相等,则证明不存在环。否则,存在环
多线程中,任务执行是随机的,那如何保证任务被依赖的任务先于任务执行呢?
这里要解决的主要有三个问题
- 首先我们要解决一个问题,当前任务有哪些前置任务,这个可以用 list 存储,代表它依赖的任务 list。当它所依赖的任务 list 没有执行完毕,当前任务需要等待。
- 当前任务执行完毕之后,所有依赖它的子任务需要感知到。我们可以用一个 map 来存储这种关系,key 是当前任务,value 是依赖于当前任务的集合(list)
- 多线程当中,等待和唤醒功能,有多种方式可以实现。wait、notify 机制,ReentrantLock Condition 机制,CountDownLatch 机制。这里我们选择 CountDownLatch 机制,因为 CountDownLatch 有点类似于计数器,特别适合这种场景。
具体实现
IAnchorTask
首先,我们定义一个 IAnchorTask 接口,主要有一个方法
isRunOnMainThread(): Boolean
表示是否在主线程运行,默认值是 falsepriority(): Int
方法 表示线程的优先级别,默认值是 Process.THREAD_PRIORITY_FOREGROUNDneedWait()
表示当我们调用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 是 AnchorTasktaskChildMap: MutableMap<Class<out AnchorTask>, ArrayList<Class<out AnchorTask>>?> = HashMap()
,储存当前任务的子任务, key 是当前任务的 class,value 是 AnchorTask 的 list
算法思想
- 首先找出所有入度为 0 的队列,用 queue 变量存储
- 当队列不为空,进行循环判断。
- 从队列 pop 出,添加到结果队列
- 遍历当前任务的子任务,通知他们的入度减一(其实是遍历 taskChildMap),如果入度为 0,添加到队列 queue 里面
- 当结果队列和 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 有点类似于装饰者模式,多线程依赖的执行关系在这里都得到体现,只有几行代码
- 前置任务没有执行完毕的话,等待,执行完毕的话,往下走
- 执行任务
- 通知子任务,当前任务执行完毕了,相应的计数器(入度数)要减一。
总结
AnchorTask 的原理不复杂,本质是有向无环图与多线程知识的结合。
- 根据 BFS 构建出有向无环图,并得到它的拓扑排序
- 在多线程执行过程中,我们是通过任务的子任务关系和 CounDownLatch 确保先后执行关系的
- 前置任务没有执行完毕的话,等待,执行完毕的话,往下走
- 执行任务
- 通知子任务,当前任务执行完毕了,相应的计数器(入度数)要减一。
AnchorTask 源码已经更新到 github,AnchorTask。
在实现这个开源框架的时候,借鉴了以下开源框架的思想。AppStartFaster 主要是通过 ClassName 找到相应的 Task,而阿里 alpha 是通过 taskName 找到相应的 Task,并且需要指定 ITaskCreator。两种方式各有优缺点,没有优劣之说,具体看使用场景。
更新说明
- 之前的 0.1.0 版本 配置前置依赖任务,是通过
AnchorTask getDependsTaskList
的方式,他是通过className
找到AnchorTask
,并且内聚在当前的 AnchorTask 中,从全局的角度看 ,这种方式不太直观,1.0.0 放弃了这种方式,参考阿里Alpha
的方式,通过addTask(TASK_NAME_THREE).afterTask(TASK_NAME_ZERO, TASK_NAME_ONE)
- 1.0.0 版本新增了 Project 类,并增加
OnProjectExecuteListener
监听 - 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 介绍
AnchorTaskDispatcher start
方法必须在主线程调用,子线程调用会抛出异常。await
阻塞当前线程,等待所有任务执行完毕之后,会自动往下走,await 方法携带一个参数,timeOutMillion 表示超时等待的时间await()
方法必须在 start 方法之后调用- 添加任务是通过
AnchorProject.Builder().addTask
添加的,典型的构造模式 - 设置执行的线程池,可以通过
AnchorProject.Builder().setThreadPoolExecutor(TaskExecutorManager.instance.cpuThreadPoolExecutor)
AnchorTask 介绍
AnchorTask 实现了 IAnchorTask 接口,主要有几个方法
isRunOnMainThread(): Boolean
表示是否在主线程运行,默认值是 falsepriority(): Int
方法 表示线程的优先级别,默认值是 Process.THREAD_PRIORITY_FOREGROUNDneedWait()
表示当我们调用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 的原理不复杂,本质是有向无环图与多线程知识的结合。
- 根据 BFS 构建出有向无环图,并得到它的拓扑排序
- 在多线程执行过程中,我们是通过任务的子任务关系和 CounDownLatch 确保先后执行关系的
- 前置任务没有执行完毕的话,等待,执行完毕的话,往下走
- 执行任务
- 通知子任务,当前任务执行完毕了,相应的计数器(入度数)要减一。