前言
App启动优化,自然会想到异步加载。将耗时任务放到子线程加载,等到所有加载任务加载完成之后,再进入首页。这种多线程异步加载方案适合于多个任务之间没有依赖关系,业务逻辑没有那么复杂情况。但是在实际项目中肯定会遇到复杂业务逻辑情况,如任务2依赖于任务1,这种情况如何解决?或者任务3依赖于任务2,任务2依赖于任务1,这种更复杂情况又该如何解决呢?
那既能够异步操作、又能解决任务之间的依赖关系,同时执行代码更加优雅的方式有没有?下面这种解决方案就具备该条件:有向无环图启动器。
重要概念
在介绍有向无环图启动器之前,先了解下有向无环图,它可以完美解决先后依赖关系。
有向无环图
有向无环图:有向无环图是有向图的一种。若一个有向图中不存在环,则称为有向无环图,也称为DAG图。常常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。
上图就是一个有向无环图图,两个顶点之间不存在相互指向的边。若该图中B->A那么就存在环了,便不是有向无环图。
基础概念 :
- 顶点
图中的点,如顶点A、顶点B
- 入度
代表当前有多少边指向它
- 出度
代表当前有多少边从它指出
在上图中,顶点A 入度为0,因为没有任何边指向它。顶点D入度为1,因为顶点A指向顶点D。顶点B入度为2,因为顶点A指向顶点B,同时顶点D指向顶点B。
拓扑排序
拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面,而且每个顶点出现且只出现一次。所以常用有向无环图的数据结构用来解决依赖关系。
拓扑排序一般有两种算法:一种是BFS算法,也叫做入度表法;另一种是DFS算法。
BFS算法(入度表法)
入度表法是根据顶点的入度来判断是否有依赖关系的。若顶点的入度不为 0,则表示它有前置依赖。它也被称作 BFS 算法。
算法思想
- 建立入度表,入度为 0 的节点先入队
- 当队列不为空,进行循环判断
- 节点出队,添加到结果 list 当中
- 将该节点的邻居入度减 1
- 若邻居节点入度为 0,加入队列
- 若结果list与所有节点数量相等,则证明不存在环。否则,存在环
从上面的入度表法,我们可以知道,要得到有向无环图的拓扑排序,我们的关键点要找到入度为0的顶点。然后接着删除该结点的相邻所有边。再遍历所有结点。直到入度为 0 的队列为空。
代码实现
示例:假设 n = 6,先决条件表:[ [3, 0], [3, 1], [4, 1], [4, 2], [5, 3], [5, 4] ];课程0,课程1,课程2没有先修课,可以直接选。其余的,都要先修2门课。用有向图描述这种依赖关系:
从上图所示,顶点 0、1、2 的 入度为 0;顶点 3、4、5 的 入度为2。
解题思路 :
- 维护一个 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) {
nDegree[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
f (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算法
与BFS不同的是,DFS的关键点在于找到,出度为0的顶点。深度优先搜索过程中,当到达出度为0的顶点时,需要进行回退。在执行回退时记录出度为0的顶点,将其入栈;则最终出栈顺序的逆序即为拓扑排序序列。
算法思想
- 对图执行深度优先搜索
- 在执行深度优先搜索时,若某个顶点不能继续前进,即顶点的出度为0,则将此顶点入栈
- 最后得到栈中顺序的逆序即为拓扑排序顺序
代码实现
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) {
// 当前节点在此次 dfs 中正在访问,说明存在环
if (status[i] == 1)
return false;
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;
}
有向无环启动器:AnchorTask
如果遇到前后依赖的关系,就可以使用AnchorTask解决,它的实现原理是构建一个有向无环图,拓扑排序之后,如果任务B依赖任务A,那么A一定排在任务B之后。
添加项目依赖
implementation 'com.xj.android:anchortask:1.0.0'
在项目应用
创建任务Task,继承 AnchorTask
public class TaskA extends AnchorTask {
public TaskA(@NotNull String name) {
super(name);
}
@Override
public void run() {
try {
Thread.sleep(80);
LogUtils.e("AnchorTask","TaskA 初始化分析统计组件");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TaskB extends AnchorTask {
public TaskB(@NotNull String name) {
super(name);
}
@Override
public void run() {
try {
Thread.sleep(300);
LogUtils.e("AnchorTask","TaskB获取支付相关配置信息");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TaskC extends AnchorTask {
public TaskC(@NotNull String name) {
super(name);
}
@Override
public void run() {
try {
Thread.sleep(800);
LogUtils.e("AnchorTask","TaskC初始化支付组件");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建3个耗时任务,模拟实际项目中遇到场景。指定 taskName,注意 taskName必须是唯一的,因为我们会根据 taskName 找到相应的AnchorTask重写相应的方法。TaskC依赖于TaskB 任务执行。
创建自定义类AnchorTaskCreator,实现IAnchorTaskCreator接口,获取任务实例
public class AnchorTaskCreator implements IAnchorTaskCreator {
@Nullable
@Override
public AnchorTask createTask(@NotNull String s) {
switch (s) {
case "TaskA":
return new TaskA("TaskA");
case "TaskB":
return new TaskB("TaskB");
case "TaskC":
return new TaskC("TaskC");
}
return null;
}
}
开启任务执行
public class AppAplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initAnchorTask();
}
private void initAnchorTask() {
AnchorProject project=new AnchorProject.Builder().setContext(this)
.setAnchorTaskCreator(new AnchorTaskCreator())
.setLogLevel(LogUtils.LogLevel.ERROR)
.addTask("TaskA")
.addTask("TaskB")
.addTask("TaskC").afterTask("TaskB")
.build();
project.start().await(500);
// 监听任务回调
project.addListener(new OnProjectExecuteListener() {
@Override
public void onProjectStart() {
LogUtils.e("Project","====>onProjectStart");
}
@Override
public void onTaskFinish(@NotNull String s) {
LogUtils.e("Project","==onTaskFinish==>"+s+"执行完毕");
}
@Override
public void onProjectFinish() {
LogUtils.e("Project","====>onProjectFinish");
}
});
// 添加每个任务执行耗时回调
project.setOnGetMonitorRecordCallback(new OnGetMonitorRecordCallback() {
@Override
public void onGetTaskExecuteRecord(@Nullable Map<String, Long> map) {
for ( String key:map.keySet()){
LogUtils.e("Project","===onGetTaskExecuteRecord=>"+key+"耗时"+map.get(key)+"ms");
}
}
@Override
public void onGetProjectExecuteTime(long l) {
LogUtils.e("Project","==onGetProjectExecuteTime==>总耗时"+l+"ms");
}
});
}
}
结果为:
AnchorTask: TaskA 初始化分析统计组件
AnchorTask: TaskB获取支付相关配置信息
AnchorTask: TaskC初始化支付组件
Project: ==onTaskFinish==>TaskC执行完毕
Project: ====>onProjectFinish
Project: ==onGetProjectExecuteTime==>总耗时1106ms
Project: ===onGetTaskExecuteRecord=>TaskB耗时301ms
Project: ===onGetTaskExecuteRecord=>TaskA耗时96ms
Project: ===onGetTaskExecuteRecord=>TaskC耗时800m
好了,欢迎点赞、评论