启动优化知识之有向无环启动器

前言

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 算法。

算法思想
  1. 建立入度表,入度为 0 的节点先入队
  2. 当队列不为空,进行循环判断
    • 节点出队,添加到结果 list 当中
    • 将该节点的邻居入度减 1
    • 若邻居节点入度为 0,加入队列
  3. 若结果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的顶点,将其入栈;则最终出栈顺序的逆序即为拓扑排序序列。

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

在这里插入图片描述

代码实现
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之后。

GitHub 地址

添加项目依赖

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

好了,欢迎点赞、评论

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值