Android startup optimization-directed acyclic graph

From 0 to 1, these articles explain how DAG directed acyclic graph is implemented and how to start optimized applications on Android.

Reason for recommendation: Nowadays, when many articles talk about startup optimization, they often talk about topology. This article explains everything clearly from data structure to algorithm to design. Open source projects also have very strong reference significance.

Preface

Speaking of Android startup optimization, everyone may think of asynchronous loading at the first time. Put time-consuming tasks into sub-threads for loading, and wait until all loading tasks are completed before entering the homepage.

The multi-threaded asynchronous loading solution is indeed ok. But what if you encounter a dependent relationship. For example, task 2 depends on task 1, how to solve it at this time.

The simplest solution is to throw task 1 to the main thread for loading, and then start multi-threaded asynchronous loading.

What if you encounter more complex dependencies.

Task 3 depends on task 2, and task 2 depends on task 1. How do you solve this problem? What about more complex dependencies?

You can't load both Task 2 and Task 3 on the main thread, so multi-thread loading is of little significance.

Is there a better solution?

The answer is definitely yes, use a directed acyclic graph. It can perfectly solve the dependency relationship.

important concepts

Directed Acyclic Graph (DAG) is a type of directed graph. The literal meaning is that there are no cycles in the graph. It is often used to express the driver dependencies between events and manage the scheduling between tasks.

Vertex: A point in the graph, such as vertex 1, vertex 2.

Edge: The line segment connecting two vertices is called edge.

In-degree: Represents how many edges currently point to it.

In the above figure, the indegree of 1 is 0 because there are no edges pointing to it. The in-degree of 2 is 1, because 1 points to 2. Similarly, the in-degree of 5 is 2, because 4 and 3 point to it.

Topological sorting: Topological sorting is the process of constructing a topological sequence for a directed graph. It has the following characteristics.

  • Each vertex appears exactly once.
  • If there is a path from vertex A to vertex B, then vertex A appears before vertex B in the sequence

Because of this characteristic, the data structure of a directed acyclic graph is often used to resolve dependencies.

In the above figure, after topological sorting, task 2 must be ranked after task 1, because task 2 depends on task 1, and task 3 must be after task 2, because task 3 depends on task 2.

There are generally two algorithms for topological sorting, the first is the in-degree table method, and the second is the DFS method. Next, let's take a look at how to implement it.

entry table method

The in-degree table method determines whether there is a dependency relationship based on the in-degree of the vertices. If the in-degree of a vertex is not 0, it means that it has previous dependencies. It is also often called the BFS algorithm

Algorithmic thinking

  • Create an in-degree table, with nodes with an in-degree of 0 joining the queue first.
  • When the queue is not empty, perform loop judgment
    • The node is dequeued and added to the result list.
    • Decrease the neighbor in-degree of the node by 1
    • If the in-degree of the neighbor node is 0, add it to the queue
  • If the resulting list is equal to the number of all nodes, it proves that there is no cycle. Otherwise, there is a cycle

Example explanation

The directed acyclic graph shown in the figure below uses the in-degree table method to obtain the topological sorting process.

!

First, we select the vertex with in-degree 0, where the in-degree of vertex 1 is 0. After deleting vertex 1, the graph becomes as follows.

At this time, the in-degrees of vertex 2 and vertex 4 are both 0, and we can delete any vertex at will. (This is why the topological ordering of the graph is not the only reason). Here we delete vertex 2, and the graph becomes as follows:

At this time, we delete vertex 4, and the graph becomes as follows:

Select vertex 3 with indegree 0, and after deleting vertex 3, the icon name is as follows,

Finally, vertex 5 is left, vertex 5 is output, and the topological sorting process ends. The final output is:

At this point, the process of the in-degree method of priority acyclic graph has been explained. You understand.

The code will be given in the next issue.

time complexity

Assuming that the AOE network has n events and e activities, the main execution of the algorithm is:

  • Find the ve value and vl value of each event: the time complexity is O(n+e);
  • Find key activities based on ve value and vl value: time complexity is O(n+e);

Therefore, the time complexity of the entire algorithm is O(n+e)

DFS algorithm

From the in-degree table method above, we can know that to obtain the topological sorting of a directed acyclic graph, our key point is to find the vertex with an in-degree of 0. Then delete all adjacent edges of the node. Then traverse all nodes. Until the queue with indegree 0 is empty. This method is actually BFS.

Speaking of BFS, we immediately think of DFS. Different from BFS, the key point of DFS is to find the vertex with out-degree 0.

To summarize, during the depth-first search process, when reaching a vertex with an out-degree of 0, a rollback is required. When performing rollback, record the vertex with out-degree 0 and push it onto the stack. Then the reverse order of the final pop order is the topological sorting sequence.

Algorithmic thinking

  • Perform a depth-first search on the graph.
  • When performing a depth-first search, if a vertex cannot move forward, that is, the out-degree of the vertex is 0, this vertex is pushed onto the stack.
  • Finally, the reverse order of the order in the stack is obtained, which is the topological sort order.

Example explanation

Similarly, the following figure explains the process of the DFS algorithm.

(1) Starting from vertex 1, start depth-first search. The order is 1->2->3->5.

(2) When the depth-first search reaches vertex 5, the out-degree of vertex 5 is 0. Push vertex 5 onto the stack.

(3) Depth-first search execution rolls back to vertex 3. At this time, the out-degree of vertex 3 is 0, so vertex 3 is pushed onto the stack.

(4) Go back to vertex 2, the out-degree of vertex 2 is 0, and vertex 2 is pushed onto the stack.

(5) Go back to vertex 1. The forward position of vertex 1 is vertex 4, and the order is 1->4.

(6) The out-degree of vertex 4 is 0, and vertex 4 is pushed into the stack.

(7) Go back to vertex 1, the out-degree of vertex 1 is 0, and vertex 1 is pushed onto the stack.

(8) The reverse order of the stack is 1->4->2->3->5. This order is the result of topological sorting.

Notice:

The stack drawn here is not accurate. In theory, 1 should be at the top and 5 at the bottom. Some people like to understand the stack from bottom to top.

time complexity

Time complexity analysis: First, the time complexity of depth-first search is O(V+E), and each time you only need to store the visited vertices into the array, it takes O(1), so the total complexity is O(V +E).

summary

Topological sorting of directed acyclic graph is actually not difficult, the difficulty is medium. Usually, we generally use the BFS algorithm to solve it, and the DFS algorithm is less commonly used.

For BFS (in-degree table method), its core idea is

  1. Select a source vertex with no input edges (in-degree 0) (if there are multiple, select any one),
  2. Delete it and its output edges. Repeat the deletion operation of source vertices until there is no source vertex with in-degree 0.
  3. Finally, the number of vertices in the graph is detected. If there are still vertices, the algorithm has no solution. Otherwise, the deletion order of vertices is the output order of topological sorting.

The principles of topological sorting and problem-solving ideas

basic concept

The English name of topological sorting is Topological sorting.

The problem to be solved by topological sorting is to sort all the nodes of a graph. Only directed acyclic graphs have topological sorting, but non-directed acyclic graphs do not.

In other words, topological sorting must satisfy the following conditions

The graph must be an acyclic directed graph. Conditions that the sequence must meet:

  • Each vertex appears exactly once.
  • If there is a path from vertex A to vertex B, then vertex A appears before vertex B in the sequence.

Actual combat

We have used the algorithm question above leetcode as an entry point to explain.

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

eg: Now you have a total of n courses to choose, recorded as 0 to n-1.

Some prerequisite courses are required before taking certain courses. For example, to study course 0, you need to complete course 1 first. We use a match to represent them: [0,1]

Given a total number of courses and their prerequisites, returns the order in which you would take all the courses.

There may be multiple correct sequences, you just need to return one. If it is not possible to complete all courses, an empty array is returned.

Example 1

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

Example 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] 。

Obviously, this problem can be solved using directed acyclic graph.

BFS algorithm

Question analysis

We first introduce a directed graph to describe dependencies

Example: Assume n = 6, prerequisite table: [ [3, 0], [3, 1], [4, 1], [4, 2], [5, 3], [5, 4] ]

  • 0, 1, 2 There are no prerequisite courses, you can take them directly. For the rest, you need to take 2 courses first
  • We use a directed graph to describe this dependency relationship (the sequence of doing things):

In a directed graph, we know that there arein-degree and out-degreeConcept:

If there is a directed edge A --> B, then this edge adds 1 out-degree to A and 1 in-degree to B. So the in-degree of vertices 0, 1, and 2 is 0. The in-degree of vertices 3, 4, and 5 is 2

Preparation before BFS

  • We care about the degree of the course - this value needs to be reduced and monitored
  • We care about the dependencies between courses - taking this course will reduce the degree of which courses
  • Therefore, we need a suitable data structure to store these relationships. This can be done through a hash table

Problem-solving ideas

  • Maintain a queue, which contains courses with degree 0
  • Select a course and dequeue it, and at the same time View the hash table to see which subsequent courses it corresponds to
  • Set the entry degree of these subsequent lessons - 1. If any of them is reduced to 0, push it into the queue.
  • When there are no more new courses with degree 0 in the queue, the queue is empty and the loop exits.
    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 solution

Algorithmic thinking

  • Perform a depth-first search on the graph.
  • When performing a depth-first search, if a vertex cannot move forward, that is, the out-degree of the vertex is 0, this vertex is pushed onto the stack.
  • Finally, the reverse order of the order in the stack is obtained, which is the topological sort order.
// 方法 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;
    }

Summarize

From a practical perspective, this blog introduces two solutions to directed acyclic graphs, the in-degree table method and the DFS method. Among them, the penetration table method is very important and must be mastered. In the next article, we will explain how to build a general framework of directed acyclic graph from the perspective of actual project practice, so stay tuned.


AnchorTask usage instructions

Introduction

Android startup optimization, you may think of asynchronous loading at the first time. Put time-consuming tasks into sub-threads for loading, and wait until all loading tasks are completed before entering the homepage.

The multi-threaded asynchronous loading solution is indeed ok. But what if there is a dependency relationship? For example, task 2 depends on task 1, how to solve it at this time.

At this time, you can use AnchorTask to solve the problem. Its implementation principle is to build a directed acyclic graph. After topological sorting, if task B depends on task A, then A must be ranked after task B.

Basic use

Step 1: Configure remote dependencies in molde build.gradle

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

Step 2: Customize AnchorTaskB, inherit AnchorTask, and rewrite the corresponding methods

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)
        }
    }

}

If task C depends on task B and task A, it can be written like this

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

}

Finally, add the task through AnchorTaskDispatcher.instance .addTask(AnchorTaskFive()) and call the start() method to start. The await() method means blocking and waiting for all tasks to be executed.

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

Introduction to AnchorTaskDispatcher

  1. AnchorTaskDispatcher startThe method must be called in the main thread, and an exception will be thrown when called by a sub-thread.
  2. setTimeOutMillionThe method is used in conjunction with the await() method. Calling it alone has no effect. It indicates the timeout period of await waiting.
  3. awaitBlocks the current thread and waits for all tasks to be executed before it will automatically go down.
  4. await()The method must be called after the start method
  5. setThreadPoolExecutorSet the thread pool for task execution

Introduction to AnchorTask

AnchorTask implements the IAnchorTask interface and has several main methods

  • isRunOnMainThread(): BooleanIndicates whether to run on the main thread. The default value is false.
  • priority(): IntMethod represents the priority level of the thread. The default value is Process.THREAD_PRIORITY_FOREGROUND
  • needWait() indicates whether we need to wait when we call AnchorTaskDispatcher await. Return true indicates that we need to wait for the completion of the task execution before the AnchorTaskDispatcher await method can continue to execute. .
  • fun getDependsTaskList(): List<Class<out AnchorTask>>?The method returns the predecessor task dependency, and the default value is to return null.
  • fun run()Method, indicating when the task is executed
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)
        )
    }

}

Listening task callbacks

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 ")
            }

        })


Teach you step by step how to implement AnchorTask

Introduction to the principle

AnchorTask, anchor task, its implementation principle is to construct a directed acyclic graph. After topological sorting, if task B depends on task A, then A must be ranked before task B.

Before you understand the principles, you must first understand some basic knowledge of directed acyclic graphs and multi-threading. Otherwise, you will basically not be able to understand the following.

a consensus

  • Predecessor task: Task 3 depends on tasks 0 and 1, then the predecessor tasks of task 3 are tasks 0 and 1
  • Subtask: After task 0 is executed, task 3 can be executed, then task 3 is called a subtask of task 0

How to build a directed acyclic graph

Here we use the BFS method to implement it. The algorithm idea is roughly as follows

  • Create an in-degree table, with nodes with an in-degree of 0 joining the queue first.
  • When the queue is not empty, perform loop judgment
    • The node is dequeued and added to the result list.
    • Decrease the neighbor in-degree of the node by 1
    • If the entry degree of the neighbor course is 0, join the queue
  • If the resulting list is equal to the number of all nodes, it proves that there is no cycle. Otherwise, there is a cycle

In multi-threading, task execution is random, so how to ensure that the task on which the task is dependent is executed before the task?

There are three main problems to be solved here

  1. First we have to solve a problem, what are the pre-tasks of the current task? This can be stored in a list, representing the list of tasks it depends on. When the task list it depends on has not been completed, the current task needs to wait.
  2. After the current task is executed, all subtasks that depend on it need to be aware of it. We can use a map to store this relationship. The key is the current task and the value is a collection (list) that depends on the current task.
  3. In multi-threading, there are many ways to implement waiting and wake-up functions. wait, notify mechanism, ReentrantLock Condition mechanism, CountDownLatch mechanism. Here we choose the CountDownLatch mechanism, because CountDownLatch is somewhat similar to a counter and is particularly suitable for this scenario.

Implementation

IAnchorTask

First, we define an IAnchorTask interface, which mainly has one method

  • isRunOnMainThread(): BooleanIndicates whether to run on the main thread. The default value is false.
  • priority(): IntMethod represents the priority level of the thread. The default value is Process.THREAD_PRIORITY_FOREGROUND
  • needWait() indicates whether we need to wait when we call AnchorTaskDispatcher await. Return true indicates that we need to wait for the completion of the task execution before the AnchorTaskDispatcher await method can continue to execute. .
  • fun getDependsTaskList(): List<Class<out AnchorTask>>?The method returns the predecessor task dependency, and the default value is to return null.
  • fun run()Method, indicating when the task is executed
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()

}

It has an implementation class AnchorTask, which adds await and countdown methods

  • await method, call it, the current task will wait
  • countdown() method, if the current counter value > 0, it will be decremented by one, otherwise, nothing will be done.
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()
    }
}

Sorting implementation

Topological sorting of acyclic graph, the BFS algorithm is used here. For details, see the AnchorTaskUtils#getSortResult method, which has three parameters

  • list stores all tasks
  • taskMap: MutableMap<Class<out AnchorTask>, AnchorTask> = HashMap()Store all tasks, key is Class, value is AnchorTask
  • taskChildMap: MutableMap<Class<out AnchorTask>, ArrayList<Class<out AnchorTask>>?> = HashMap(), stores the subtasks of the current task, key is the class of the current task, value is the list of AnchorTask

Algorithmic thinking

  1. First find all queues with an in-degree of 0 and store them in the queue variable
  2. When the queue is not empty, loop judgment is performed.
    • Pop from the queue and add to the result queue
    • Traverse the subtasks of the current task and notify them that their in-degree is reduced by one (actually traversing taskChildMap). If the in-degree is 0, add it to the queue queue
  3. When the result queue and list size are not equal, try to prove that there is a cycle
    @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 This class is very important. Topological sorting of directed acyclic graphs and multi-thread dependent wake-up are all completed with the help of this core class.

It mainly has several member variables

// 存储所有的任务
    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()

It has a more important method setNotifyChildren(anchorTask: AnchorTask) , which has a method parameter AnchorTask. Its function is to notify the subtasks of the task that the current task has been executed and the entry degree is reduced by one.

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

Next let’s take a look at the start method

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
    }

It mainly does several things

  • Checking whether it is in the main thread does not throw an exception. Why do we need to check whether it is in the main thread? Mainly the process of building a directed acyclic graph, we must ensure that it is thread-safe
  • Get topological sorting of directed acyclic graph
  • Execute the corresponding task according to the sorting result of topological sorting. You can see that when executing the task, we use AnchorTaskRunnable to wrap it
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 is somewhat similar to the decorator pattern. The execution relationships of multi-thread dependencies are reflected here, with only a few lines of code.

  1. If the prerequisite tasks are not completed, wait. If they are completed, go down.
  2. perform tasks
  3. Notify the subtask that the current task has been executed, and the corresponding counter (in-degree) should be decremented by one.

Summarize

The principle of AnchorTask is not complicated. Its essence is the combination of directed acyclic graph and multi-threading knowledge.

  1. Construct a directed acyclic graph based on BFS and obtain its topological sorting
  2. During multi-thread execution, we ensure the sequential execution relationship through the sub-task relationship of the task and CounDownLatch.
    1. If the prerequisite tasks are not completed, wait. If they are completed, go down.
    2. perform tasks
    3. Notify the subtask that the current task has been executed, and the corresponding counter (in-degree) should be decremented by one.

AnchorTask source code has been updated to github,AnchorTask

When implementing this open source framework, we drew on the ideas of the following open source frameworks. AppStartFaster mainly finds the corresponding Task through ClassName, while Alibaba alpha finds the corresponding Task through taskName, and ITaskCreator needs to be specified. Both methods have their own advantages and disadvantages. There is no right or wrong. It depends on the usage scenario.

android-startup

alpha

AppStartFaster


Release Notes

  1. The previous 0.1.0 version configured pre-dependency tasks through AnchorTask getDependsTaskList, which was found through , and is cohesive in the current AnchorTask. From a global perspective, this method is not very intuitive. 1.0.0 abandoned this method. Refer to Ali's method, through classNameAnchorTaskAlphaaddTask(TASK_NAME_THREE).afterTask(TASK_NAME_ZERO, TASK_NAME_ONE)
  2. Version 1.0.0 adds a new Project class and adds OnProjectExecuteListener monitoring
  3. New in version 1.0.0 OnGetMonitorRecordCallback monitoring to facilitate statistics of the time spent on each task

illustrate

Android startup optimization, you may think of asynchronous loading at the first time. Put time-consuming tasks into sub-threads for loading, and wait until all loading tasks are completed before entering the homepage.

The multi-threaded asynchronous loading solution is indeed ok. But what if there is a dependency relationship? For example, task 2 depends on task 1, how to solve it at this time.

Suppose we have a task dependency like this

How do we use it?

        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
    }

}

Run the demo and you can see the expected effect.

Basic use

Step 1: Configure remote dependencies in molde build.gradle

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

The latest version number can be found here lastedt version

Step 2: CustomizeAnchorTaskZero, inheritAnchorTask, and specifytaskName, note< a i=4> must be unique, because we will find the corresponding based on and rewrite the corresponding methodtaskName taskNameAnchorTask

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)
        )
    }
}

If task three depends on task two and task one, it can be written like this

addTask(TASK_NAME_THREE).afterTask(TASK_NAME_ZERO, TASK_NAME_ONE)

Finally, start through the project.start() method. If you need to block and wait, call the await() method

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()

Listen for task callbacks

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 ")
            }

        })

Add a time-consuming callback for each task execution

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)
            }

        }

Introduction to AnchorProject

  1. AnchorTaskDispatcher startThe method must be called in the main thread, and an exception will be thrown when called by a sub-thread.
  2. awaitBlock the current thread and wait for all tasks to be executed. It will automatically go down. The await method carries a parameter, and timeOutMillion represents the timeout waiting time.
  3. await()The method must be called after the start method
  4. The added task is added through AnchorProject.Builder().addTask, a typical construction mode
  5. To set the thread pool for execution, you can passAnchorProject.Builder().setThreadPoolExecutor(TaskExecutorManager.instance.cpuThreadPoolExecutor)

Introduction to AnchorTask

AnchorTask implements the IAnchorTask interface and has several main methods

  • isRunOnMainThread(): BooleanIndicates whether to run on the main thread. The default value is false.
  • priority(): IntMethod represents the priority level of the thread. The default value is Process.THREAD_PRIORITY_FOREGROUND
  • needWait() indicates whether we need to wait when we call AnchorTaskDispatcher await. Return true indicates that we need to wait for the completion of the task execution before the AnchorTaskDispatcher await method can continue to execute. .
  • fun run()Method, indicating when the task is executed
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)
        )
    }

}

Listening task callbacks

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 ")
            }

        })

Summarize

The principle of AnchorTask is not complicated. Its essence is the combination of directed acyclic graph and multi-threading knowledge.

  1. Construct a directed acyclic graph based on BFS and obtain its topological sorting
  2. During multi-thread execution, we ensure the sequential execution relationship through the sub-task relationship of the task and CounDownLatch.
    1. If the prerequisite tasks are not completed, wait. If they are completed, go down.
    2. perform tasks
    3. Notify the subtask that the current task has been executed, and the corresponding counter (in-degree) should be decremented by one.

AnchorTask

Guess you like

Origin blog.csdn.net/jdsjlzx/article/details/135031064