android startup optimization - complete analysis of asynchronous startup framework Alpha

background

Start optimization, in fact, is to optimize the speed of the process from clicking the icon to displaying the main page, so that the main interface can be displayed in front of the user as quickly as possible. So what we have to do is to find those time-consuming operations and optimize them.

How to find the time-consuming operation? Generally divided into two scenarios:

1. Offline (debug) scenarios In the development stage of the application, we generally use AOP to perform time-consuming statistics of functions. Through the aspectj library, we can easily insert code into the function, so as to count the time-consuming time of each method. Or directly use the Profiler CPU tool that comes with Android Studio to view the time-consuming and CPU information of each method.

2. Online scenarios When the application has been released online, statistics become not so easy. Therefore, we generally use function instrumentation to write a statistical time-consuming tool class and deploy it to places that need statistics, such as the life cycle of Application and Activity, initialization of database, initialization of third-party libraries, and finally upload data to the server.

How to optimize and solve it after finding the time-consuming place?

Generally, by analyzing these tasks to be performed, asynchronous, lazy loading, preloading and other operations are performed.

Among them, "asynchronous task" is a very important part, which involves what we are going to talk about today, the launcher. As the name suggests, it is a tool to help us optimize the startup, which can efficiently and reasonably help us arrange some tasks during the startup process.

Next, let’s take everyone to analyze from the source code, let’s take a look at Ali’s launcher—— Alpha.

As a launcher, what functions should it have

Some people may ask, isn't it just an asynchronous task? I'll fix a few threads and just throw the task into it.

Things are not that simple. For example, now there are 6 tasks that need to be executed in the Application, among which "Task1, Task6" needs to be executed in the main thread, "Task2, Task3" needs to be executed after "Task1" is executed, and "Task4, Task5" "Task2 and Task3" need to be executed before it can be executed, "Task6" needs "Task4 and Task5" to be executed before it can be executed, and "Task4" takes more time than "Task5".

What is this? I faint. So many relationships, how should I deal with them? Since the text looks too troublesome, let's draw a picture, which involves a graphic for time management-Pert diagram. The Pert graph is a directed graph that can clearly describe the "dependency" between subtasks. For example, the situation of our project is drawn as a Pert diagram as follows:

Pert diagram.jpg

Through the Pert diagram, we can still see the relationship between each Task very intuitively. After executing Task2 and Task3, we have two choices, execute Task4 first or execute Task5 first. Since Task4 takes more time than Task5, we Just choose to execute Task4 first.

In fact, the plan of "making tasks and executing them" can be seen everywhere in our lives. For example, we have many things to deal with after we get up early, such as boiling water (5 minutes), brushing teeth (3 minutes), washing our face (2 minutes), and going to the toilet (8 minutes). minute). How to choose an "optimal route" to allow us to complete these things as quickly as possible? It must be that things that can be paralleled together are arranged together, and then things that take a long time to happen in parallel at the same time. For example, boil water first, then brush your teeth and wash your face while going to the toilet? Pull away, pull away, hahaha, close!

Well, let's see how we write it if we implement it with the Alpha framework?

        //Construction method, true is executed by the main thread
        Task1=new Task("task1",true);
        Task2=new Task("task2",false);
        Task3=new Task("task3",false);
        Task4=new Task("task4",true);
        Task5=new Task("task5",false);
        Task6=new Task("task6",true);
        
        //Set priority, time-consuming operation has higher priority
        Task4.setExecutePriority(1);
        Task5.setExecutePriority(2);
        
        Project.Builder builder = new Project.Builder().withTaskCreator(new MyTaskCreator());
        builder.add(Task1);
        builder.add(Task2).after(Task1);
        builder.add(Task3).after(Task1);
        builder.add(Task4).after(Task2,Task3);
        builder.add(Task5).after(Task2,Task3);
        builder.add(Task6).after(Task4,Task5);
        builder.setProjectName("innerGroup");
        
        AlphaManager.getInstance(mContext).addProject(builder.create());
        
        AlphaManager.getInstance(mContext).start();

Done! its not bad, right. Then let's analyze it together!

First of all, if we think about it ourselves, if we make an asynchronous startup framework, what issues need to be considered?

  • Multi-thread management

  • task priority

  • sequence of tasks

  • Whether the task needs to be executed on the main thread

  • multiprocessing

Let's take a look at Alpha's internal source code with these questions in mind.

Alpha source code analysis

As can be seen from the above code, the start of the task is called by the AlphaManager.getInstance(mContext).start() method, so we start to study from this start method:

        public void start() {
        Project project = null;

        do {
            //1. Whether there is a Project configured separately for the current process, this is the highest priority
            if (mProjectForCurrentProcess != null) {
                project = (Project) mProjectForCurrentProcess;
                break;
            }

            //2. If it is currently the main process, is the main process Project configured?
            if (AlphaUtils.isInMainProcess(mContext)
                    && mProjectArray.indexOfKey(MAIN_PROCESS_MODE) >= 0) {
                project = (Project) mProjectArray.get(MAIN_PROCESS_MODE);
                break;
            }

            //3. If it is a non-main process, is there a Project that configures the non-main process?
            if (!AlphaUtils.isInMainProcess(mContext)
                    && mProjectArray.indexOfKey(SECONDARY_PROCESS_MODE) >= 0) {
                project = (Project) mProjectArray.get(SECONDARY_PROCESS_MODE);
                break;
            }

            //4. Is there a Project that is applicable to all processes?
            if (mProjectArray.indexOfKey(ALL_PROCESS_MODE) >= 0) {
                project = (Project) mProjectArray.get(ALL_PROCESS_MODE);
                break;
            }
        } while (false);

        if (project != null) {
            addListeners(project);
            project.start();
        } else {
            AlphaLog.e(AlphaLog.GLOBAL_TAG, "No startup project for current process.");
        }
    }

Wow, we solved our multi-process doubts from the very beginning. The start method first judges the current process and whether it can match the tasks of the related process. You can see that there are three process configuration variables:

  • MAIN_PROCESS_MODE : Main process task

  • SECONDARY_PROCESS_MODE : Non-main process tasks

  • ALL_PROCESS_MODE: Tasks for all processes

So where to configure these process options? addProject method

      public void addProject(Task project, int mode) {
        if (project == null) {
            throw new IllegalArgumentException("project is null");
        }

        if (mode < MAIN_PROCESS_MODE || mode > ALL_PROCESS_MODE) {
            throw new IllegalArgumentException("No such mode: " + mode);
        }

        if (AlphaUtils.isMatchMode(mContext, mode)) {
            mProjectArray.put(mode, project);
        }
    }

ok, simple enough. Continue to look at the start method. Jump to the start method of the project:

    @Override
    public void start() {
        mStartTask.start();
    }

Is it so simple, just start a mStartTask? Is this mStartTask the first task among those previously set? Then look at:

        //Project.java
        private void init() {
        ...
            mProject = new Project();
            mFinishTask = new AnchorTask(false, "==AlphaDefaultFinishTask==");
            mFinishTask.setProjectLifecycleCallbacks(mProject);
            mStartTask = new AnchorTask(true, "==AlphaDefaultStartTask==");
            mStartTask.setProjectLifecycleCallbacks(mProject);
            mProject.setStartTask(mStartTask);
            mProject.setFinishTask(mFinishTask);
       ...
        }
        
        
    private static class AnchorTask extends Task {
        private boolean mIsStartTask = true;
        private OnProjectExecuteListener mExecuteListener;

        public AnchorTask(boolean isStartTask, String name) {
            super(name);
            mIsStartTask = isStartTask;
        }

        public void setProjectLifecycleCallbacks(OnProjectExecuteListener callbacks) {
            mExecuteListener = callbacks;
        }

        @Override
        public void run() {
            if (mExecuteListener != null) {

                if (mIsStartTask) {
                    mExecuteListener.onProjectStart();
                } else {
                    mExecuteListener.onProjectFinish();
                }
            }
        }

    }        

It can be seen that in the initialization method of the Project class, a start task and an end task are defined. This is because from an execution perspective, a task sequence must have a start node and an end node. But in reality, there may be multiple tasks that can start at the same time, and multiple tasks can be used as the end point at the same time. Therefore, these two nodes are set to facilitate the control of the entire process, mark the beginning and end of the process, and facilitate the monitoring of tasks.

Speaking of the above, where did the start method of starting the task go? Naturally, it is the parent class Task of AnchorTask, look at the source code:

       public synchronized void start() {
        ...
        switchState(STATE_WAIT);

        if (mInternalRunnable == null) {
            mInternalRunnable = new Runnable() {
                @Override
                public void run() {
                    android.os.Process.setThreadPriority(mThreadPriority);
                    long startTime = System.currentTimeMillis();

                    switchState(STATE_RUNNING);
                    Task.this.run();
                    switchState(STATE_FINISHED);

                    long finishTime = System.currentTimeMillis();
                    recordTime((finishTime - startTime));

                    notifyFinished();
                    recycle();
                }
            };
        }

        if (mIsInUiThread) {
            sHandler.post(mInternalRunnable);
        } else {
            sExecutor.execute(mInternalRunnable);
        }
    }

The source code is quite simple. It defines a Runnable, then judges whether it is the main thread, and executes the Runnable. Some state changes are also interspersed in it. Inside the Runnable, Task.this.run() is mainly executed, that is, the task itself is executed. Among them, the setThreadPriority method is mainly to set the priority of the thread, such as THREAD_PRIORITY_DEFAULT, etc. The priority here is higher than that of the thread, mainly for the competition of CPU resources, and has little to do with the priority between the tasks we need. If it is a task that needs to be executed on the main thread, the event will be passed to the main thread for execution through the Handler (sHandler). If it is a task that needs to be executed in a non-main thread, the thread task will be executed through the thread pool (sExecutor).

Eh, it seems to be gone? Is it gone after starting the task? Looking back, there is also a notifyFinished method. According to this name, it should be a method to notify the end of the task, look at the source code:

    void notifyFinished() {
        if (!mSuccessorList.isEmpty()) {
            AlphaUtils.sort(mSuccessorList);

            for (Task task : mSuccessorList) {
                task.onPredecessorFinished(this);
            }
        }

        if (!mTaskFinishListeners.isEmpty()) {
            for (OnTaskFinishListener listener : mTaskFinishListeners) {
                listener.onTaskFinish(mName);
            }

            mTaskFinishListeners.clear();
        }
    }

This method mainly does three things:

  • mSuccessorList Sort

  • Traverse the mSuccessorList list and execute the onPredecessorFinished method

  • Listening callback onTaskFinish method

What is mSuccessorList? We call it the "next task list", which is the list of tasks to be performed next. So the process is to sort the task list after the current task first, and sort according to the priority. Then execute the onPredecessorFinished method sequentially.

If the following task list is empty, it means that there are no follow-up tasks, then the onTaskFinish callback method will be used to inform that the current Project has been executed.

Next, let's see how the follow-up tasks are added. And how should they be sorted? What does the onPredecessorFinished method do?

    //1. Subsequent tasks added
    public Builder after(Task task) {
        task.addSuccessor(mCacheTask);
        mFinishTask.removePredecessor(task);
        mIsSetPosition = true;
        return Builder.this;
    }
        
    void addSuccessor(Task task) {
        task.addPredecessor(this);
        mSuccessorList.add(task);
    }
      
    //2. Sorting the next task list 
    public static void sort(List<Task> tasks) {
        if (tasks.size() <= 1) {
            return;
        }
        Collections.sort(tasks, sTaskComparator);
    }    
    
    private static Comparator<Task> sTaskComparator = new Comparator<Task>() {
        @Override
        public int compare(Task lhs, Task rhs) {
            return lhs.getExecutePriority() - rhs.getExecutePriority();
        }
    };    
    
    //3. Follow-up task execution
    synchronized void onPredecessorFinished(Task beforeTask) {

        if (mPredecessorSet.isEmpty()) {
            return;
        }

        mPredecessorSet.remove(beforeTask);
        if (mPredecessorSet.isEmpty()) {
            start();
        }

    }    
      

ok, the source code is written very clearly, here is a step-by-step analysis:

According to the source code, the following task list is mainly through the after method. Do you remember when you configured the task before? builder.add(Task2).after(Task1), so this after means that Task2 will be executed after Task1, that is, Task2 becomes the next task of Task1. Similarly, Task1 becomes the immediate predecessor of Task2. That is, the addPredecessor method in the code, while adding the successor task, it also adds the predecessor task.

Some people may ask, what is the use of adding the previous task? Is it possible to go back and execute it? Just imagine, what if there are multiple tasks with one follow-up task? For example in this case: builder.add(Task4).after(Task2,Task3). Task4 is the successor task of Task2 and Task3, so after Task2 is executed, it is necessary to judge whether Task3 is successfully executed, and then Task4 can be executed. This is the role of the immediate preceding task list. This also corresponds to the logic of the onPredecessorFinished method in the above code.

Then how is the sorting of the following task list arranged? In fact, the execution priority number of the task is obtained through the getExecutePriority method, and it is arranged in a positive order. The smaller the task, the earlier the execution time. Remember when I set the setExecutePriority method in the previous configuration, it is here that the priority is set.

So far the main logic is almost the same. Seems pretty simple, doesn't it? I also briefly mention some details:

  • Various callbacks: including some task callbacks and project callbacks.

  • Logging: For example, the time-consuming record, the recordTime method when the task was executed just now is to record the time-consuming of each task.

  • Various Task configuration methods: In addition to the Java code configuration above, you can also configure the Project and the Task inside through the xml file. This is mainly the XmlPullParser class to parse the xml data, and then generate the Prject.

  • Various design patterns: such as the builder pattern for building a Project, and the factory pattern for creating a Task by passing in the task name.

Friends who are interested in some details such as this can download the source code to see for themselves.

Finally, let's summarize it with a flow chart:

Summarize

After analysis, this asynchronous startup framework should be relatively simple, but it can solve the problem! In fact, we can also do some accumulation in our usual work, and then write it into a tool or framework. If it can be open sourced and used by everyone, it is really a good thing!

Notice:

In addition to Ali's Alpha framework, B ilibili has also open sourced a similar framework: android-startup , this framework is relatively simpler to use, see: https://blog.csdn.net/jdsjlzx/article/details/128984147

Tags: Android

Posted by liquidchild_au on Tue, 14 Feb 2023 11:42:02 +1030