Android performance optimization Why do you need to start optimization

1. Why start optimization

The first impression of an app is very important, and the first impression often determines whether the user will stay or not. Opening an app, if the speed is fast and smooth, it is easy to make people think that the technical strength behind the app is strong, and users will subconsciously trust the app more.

Secondly, there is also a popular saying on the Internet, which is the 8-second rule, which means that if a user opens a page and does not open it within 8 seconds, the user will probably give up, which means the loss of a user. From here we can see the importance of startup optimization.

2. Started categories

2.1 Cold start

Let’s take a look at the flow chart of cold start

As can be seen from the figure, the process of APP startup is: ActivityManagerProxy calls AMS (ActivityManagerService) through IPC, AMS starts an APP process through IPC, ApplicationThread creates Application and binds it through reflection, and finally controls the life cycle of activity through ActivityThread , in the life cycle of the relevant page to implement the view through ViewRootImpl. In this way, the startup of the application is completed.

2.2 Warm Start

The speed of hot start is the fastest, and it is a process in which the process switches from the background to the foreground.

2.3 Warm start

Warm startup will only go through the life cycle of the page again, but for the process, the application will not be recreated.

3. Optimization direction

The above describes several ways to start. It can be seen that we optimize the start-up, basically just optimize the cold start. But many of the startup process from the cold start are done by the system, and we have no way to control it. What we can do is the life cycle of the application and the life cycle of the activity. Startup optimization often starts with these two parts.

4. Measurement method of startup time

4.1 Use adb command mode (easy to use offline)

adb shell am start -W package name/package name + class name

ThisTime: The startup time of the last activity

TotalTime: The startup time of all activities

WaitTime: the total time spent by AMS to start the activity

Here, since I directly enter the main interface, there is no SplashActivity in the middle, so the time of thisTime and TotalTime are the same

Advantages: easy to use offline, suitable for running offline products, and the time to obtain competing products, and then compare

Disadvantages: It cannot be brought online, and the time of acquisition can only be said to be an approximate time, which is not very rigorous.

4.2 Manual dot method

Timestamp via System.currentTimeMillis()

Disadvantages: Obviously, it is very intrusive to the code. If I want to print the time spent on each task, the code looks disgusting

5. Elegant acquisition method takes time

5.1 AOP Aspect Oriented Programming

AOP: A technology that achieves unified maintenance of program functions through pre-compilation and runtime dynamic proxy. Its core idea is to separate the business logic processing part of the application from the part that provides general services to it, that is, “cross-cutting concerns”.

OOP: Introduce concepts such as encapsulation, inheritance, and polymorphism to establish an object hierarchy, which allows developers to define vertical relationships, but is not suitable for horizontal relationships.

It can be said that AOP is a supplement and perfection of OOP.

5.2 Use of aspectj

AspectJ is an aspect-oriented programming framework, which is an extension of java and is compatible with java. AspectJ defines the AOP syntax, and it has a special compiler to generate Class files that comply with the java byte encoding specification.

Add dependencies to build.gradle in the root directory of the project:

dependencies {
        classpath 'com.android.tools.build:gradle:3.5.2'
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'// NOTE: Do not place your application dependencies here; they belong// in the individual module build.gradle files
    }

Add dependencies to build.gradle under app

apply plugin: 'android-aspectjx'

add in dependencies

implementation 'org.aspectj:aspectjrt:1.9.4'

then create a class

package com.noahedu.myapplication.aspectj;

import android.util.Log;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

/**
 * @Description: //@Before runs before the entry point
 * // @After("")
 * // @Around runs before and after the entry point
 * @Author: huangjialin
 * @CreateDate: 2020/7/10 14:07
 */
@Aspect
public class MyApplicationAspectj {

    @Around("call(* com.noahedu.myapplication.MyApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint){
        Signature signature = joinPoint. getSignature();
        String name = signature. getName();
        long time = System. currentTimeMillis();
        try {
            joinPoint. proceed();
        } catch (Throwable throwable) {
            throwable. printStackTrace();
        }
        Log.e("MyApplicationAspectj " ,(name + " cost " + (System.currentTimeMillis() - time)));
    }
}

In this way, when we run, we will directly print out the time-consuming situation of all calling methods in the onCreate method in the application in the logcat

2020-07-1014:22:27.1511619-1619/? E/MyApplicationAspectj: taskOne cost 1502020-07-1014:22:29.2031619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskTwo cost 2052201 :22:29.5541619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskThrid cost 3512020-07-1014:22:30.5561619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskFour cost 1001

In this way, we hardly touch any code in the Application, and it is enough to get the time-consuming of each method, and there is almost no intrusion into the code.

6. Start optimization tool selection

6.1 traceview

TraceView is a built-in tool in the Android SDK. It can load the trace file and display the execution time, times and call stack of the corresponding code in a graphical form, which is convenient for our analysis.

 Debug. startMethodTracing("MyApplication");
      //TODO
 Debug.stopMethodTracing();

Run the project to find the corresponding trace file in our SD card. If it is Android studio, you can find it directly in the lower right corner

DeviceFileExporer –>sdcard –> Android — > data –>files —> package name of your own project

Then double click to view the file

Advantages: easy to use, graphical display of execution time, call stack, etc.

Disadvantages: It will affect the direction of our optimization. Because it is a graphical display, it also takes up CPU resources, so the obtained time is often longer than the actual one.

7. Launcher

The above introduces a few more ways and tools to obtain task execution time, so when we know that a certain method is time-consuming, what should we do?

package com.noahedu.myapplication;

import android.app.Application;
import android.os.Debug;
import android.util.Log;


/**
 * @Description: java class description
 * @Author: huangjialin
 * @CreateDate: 2020/7/10 9:59
 */
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Debug.startMethodTracing("MyApplication");
        taskOne();
        taskTwo();
        taskThrid();
        taskFour();
        Debug. stopMethodTracing();
    }

    public void taskOne(){
        try {
            Thread. sleep(150);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void taskTwo(){
        try {
            Thread. sleep(2050);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void taskThrid(){
        try {
            Thread. sleep(350);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void taskFour(){
        try {
            Thread. sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Now there are several tasks in the onCreate method of the application, and the time consumption of each is different. Maybe many students will say, asynchronous processing, open a thread, put it in the thread pool, or create an IntentService to execute. Then we have to consider a few issues

First: asynchronous processing, what if a certain SDK needs to be used on a certain page, but the initialization has not been completed?

Second: What if taskTwo needs a certain return value from taskOne? How to ensure that taskOne is executed before taskTwo?

Third: Open threads, how many threads are open? Too much will cause waste of resources, and less resources will not be used rationally.

I personally think that for startup optimization, onCreate() in Application initializes task operations. We first need to prioritize these tasks. For those tasks with high priority, we can give priority to processing. For those tasks with priority For lower-level ones, we can load them with appropriate delay.

Secondly, many students like to delay loading tasks with lower priority, such as new Handler().postDelayed(). I think this is very undesirable. If the task placed in postDelayed takes 2s, the delay 1s for processing, then in the process of executing the 2s task, if there is a user to operate, wouldn’t it be very stuck? Obviously, this is not a permanent cure for the indicator.

7.1 The idea of the launcher

In view of the pain points mentioned above, how to deal with the above pain points and ensure the maintainability of the code? In other words, a newcomer does not need to understand the whole process, but can start working directly? Well, here comes the launcher.

The core idea of the launcher: make full use of CPU multi-core, and automatically sort out the task sequence

7.2 The principle of the launcher

1. All tasks are encapsulated into Task objects and passed into the collection.

2. According to all task dependencies, a directed acyclic graph is formed, and then the execution flow of tasks is arranged by topological sorting

3. Use CountDownLatch to control whether a certain task is executed before proceeding to the next step.

4. The number of core threads created by the thread pool is determined by the number of cores of the mobile phone.

7.3 How to use the launcher

7.4 launcher core code

Sort tasks

package com.noahedu.launchertool.launchstarter.sort;


import com.noahedu.launchertool.launchstarter.task.Task;
import com.noahedu.launchertool.launchstarter.utils.DispatcherLog;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import androidx.annotation.NonNull;
import androidx.collection.ArraySet;


public class TaskSortUtil {

    private static List<Task> sNewTasksHigh = new ArrayList<>();// High priority Task/**
     * Topological sorting of directed acyclic graphs of tasks
     *
     * @return
     */
    public static synchronized List<Task> getSortResult(List<Task> originTasks,
                                                        List<Class<? extends Task>> clsLaunchTasks) {
        long makeTime = System. currentTimeMillis();

        Set<Integer> dependSet = new ArraySet<>();
        Graph graph = new Graph(originTasks. size());
        for (int i = 0; i < originTasks. size(); i ++ ) {
            Task task = originTasks. get(i);
            if (task.isSend() || task.dependsOn() == null || task.dependsOn().size() == 0) {
                continue;
            }
            for (Class cls : task. dependsOn()) {
                int indexOfDepend = getIndexOfTask(originTasks, clsLaunchTasks, cls);
                if (indexOfDepend < 0) {
                    throw new IllegalStateException(task. getClass(). getSimpleName() +
                            " depends on " + cls. getSimpleName() + " can not be found in task list ");
                }
                dependSet.add(indexOfDepend);
                graph. addEdge(indexOfDepend, i);
            }
        }
        List<Integer> indexList = graph. topologicalSort();
        List<Task> newTasksAll = getResultTasks(originTasks, dependSet, indexList);

        DispatcherLog.i("task analyze cost makeTime " + (System. currentTimeMillis() - makeTime));
        printAllTaskName(newTasksAll);
        return newTasksAll;
    }

    @NonNull
    private static List<Task> getResultTasks(List<Task> originTasks,
                                             Set<Integer> dependSet, List<Integer> indexList) {
        List<Task> newTasksAll = new ArrayList<>(originTasks. size());
        List<Task> newTasksDepended = new ArrayList<>();// Depended on by others
        List<Task> newTasksWithOutDepend = new ArrayList<>();// No dependencies
        List<Task> newTasksRunAsSoon = new ArrayList<>();// Those that need to increase their priority should be executed first (this is first relative to those without dependencies) for (int index : indexList) {
            if (dependSet. contains(index)) {
                newTasksDepended.add(originTasks.get(index));
            } else {
                Task task = originTasks. get(index);
                if (task. needRunAsSoon()) {
                    newTasksRunAsSoon. add(task);
                } else {
                    newTasksWithOutDepend. add(task);
                }
            }
        }
        // Order: Those who are dependent on others-------------------------------------------------------------------
        sNewTasksHigh.addAll(newTasksDepended);
        sNewTasksHigh.addAll(newTasksRunAsSoon);
        newTasksAll. addAll(sNewTasksHigh);
        newTasksAll.addAll(newTasksWithOutDepend);
        return newTasksAll;
    }

    private static void printAllTaskName(List<Task> newTasksAll) {
        if (true) {
            return;
        }
        for (Task task : newTasksAll) {
            DispatcherLog.i(task.getClass().getSimpleName());
        }
    }

    public static List<Task> getTasksHigh() {
        return sNewTasksHigh;
    }

    /**
     * Get the index of the task in the task list
     *
     * @param originTasks
     * @return
     */
    private static int getIndexOfTask(List<Task> originTasks,
                                      List<Class<? extends Task>> clsLaunchTasks, Class cls) {
        int index = clsLaunchTasks. indexOf(cls);
        if (index >= 0) {
            return index;
        }

        // only protective code
        final int size = originTasks. size();
        for (int i = 0; i < size; i ++ ) {
            if (cls. getSimpleName(). equals(originTasks. get(i). getClass(). getSimpleName())) {
                return i;
            }
        }
        return index;
    }
}

Execute task code

package com.noahedu.launchertool.launchstarter.task;

import android.os.Looper;
import android.os.Process;

import com.noahedu.launchertool.launchstarter.TaskDispatcher;
import com.noahedu.launchertool.launchstarter.stat.TaskStat;
import com.noahedu.launchertool.launchstarter.utils.DispatcherLog;

/**
 * Where the task is actually performed
 */

public class DispatchRunnable implements Runnable {
    private Task mTask;
    private TaskDispatcher mTaskDispatcher;

    public DispatchRunnable(Task task) {
        this.mTask = task;
    }
    public DispatchRunnable(Task task, TaskDispatcher dispatcher) {
        this.mTask = task;
        this.mTaskDispatcher = dispatcher;
    }

    @Override
    public void run() {
        DispatcherLog.i(mTask.getClass().getSimpleName()
                 + "begin run" + "Situation " + TaskStat. getCurrentSituation());

        Process.setThreadPriority(mTask.priority());

        long startTime = System. currentTimeMillis();

        mTask.setWaiting(true);
        mTask.waitToSatisfy();

        long waitTime = System. currentTimeMillis() - startTime;
        startTime = System. currentTimeMillis();

        // Execute Task
        mTask.setRunning(true);
        mTask. run();

        // Execute the tail task of Task
        Runnable tailRunnable = mTask. getTailRunnable();
        if (tailRunnable != null) {
            tailRunnable. run();
        }

        if (!mTask.needCall() || !mTask.runOnMainThread()) {
            printTaskLog(startTime, waitTime);

            TaskStat. markTaskDone();
            mTask. setFinished(true);
            if(mTaskDispatcher != null){
                mTaskDispatcher.satisfyChildren(mTask);
                mTaskDispatcher.markTaskDone(mTask);
            }
            DispatcherLog.i(mTask.getClass().getSimpleName() + "finish");
        }
    }

    /**
     * Print out the log of Task execution
     *
     * @param startTime
     * @param waitTime
     */
    private void printTaskLog(long startTime, long waitTime) {
        long runTime = System. currentTimeMillis() - startTime;
        if (DispatcherLog. isDebug()) {
            DispatcherLog.i(mTask.getClass().getSimpleName() + "wait" + waitTime + "run"
                     + runTime + " isMain " + (Looper. getMainLooper() == Looper. myLooper())
                     + " needWait " + (mTask. needWait() || (Looper. getMainLooper() == Looper. myLooper()))
                     + " ThreadId " + Thread.currentThread().getId()
                     + " ThreadName " + Thread.currentThread().getName()
                     + " Situation " + TaskStat. getCurrentSituation()
            );
        }
    }

}

The basic core code is the above, and the complete code will be given in the demo later

8. Other optimization schemes

8.1 Initialize delayed tasks in batches, use the IdleHandler feature to perform idle execution (suitable for third-party SDKs with low priority and no rush to initialize)

IdleHandler: IdleHandler can be used to improve performance, mainly when we want to be able to do something when the current thread message queue is idle (for example, after the UI thread is displayed, if the thread is idle, we can prepare other content in advance), but It is best not to do time-consuming operations. To put it simply, the task in IdleHandler will be executed when the looper object is free.

As mentioned earlier, the tasks in the application are prioritized, so if the tasks with low priority, can we not initialize them in the onCreate of the application? Can it be placed in an activity? If it can be in the activity, is it suitable for that stage of the activity? In fact, both onCreate() and onResume() are possible. Of course, we can also use getViewTreeObserver().addOnPreDrawListener in the view to listen. This event is called back when the view is about to be drawn. We can enlarge the SDK to be initialized in IdleHandler in the callback method.

8.2 Loading SP in advance can be loaded before multidex, using the CPU at this stage

SharedPreferences, which store data in the form of key-value pairs, will be loaded into memory at one time, so we can consider that the CPU at that stage is relatively idle. If it can be loaded before multidex, make full use of the CPU at this stage for