Interviewer: Talk about Android startup optimization

The startup speed of an application is very important to an APP, and it will directly affect the user experience. If the startup speed is too slow, it will lead to the loss of users. This article analyzes the startup speed optimization and provides some ideas for optimizing the startup speed.

1. Application startup classification

The application has three startup states, each of which affects the time required for the application to be displayed to the user: cold startup, warm startup, and hot startup. In a cold start, the application starts from the beginning. In the other two states, the system needs to bring applications running in the background to the foreground.

1.1, cold start

App cold start can be divided into two stages

The first stage

1. Load and start the app

2. A blank startup window will be displayed immediately after startup

3. Create the app process

second stage

1. Create an app object

2. Start the main thread

3. Create the main Activity

4. Load the layout

5. Layout the screen

6. Draw the first frame

Once the application process finishes drawing the first frame, the system process will replace the currently displayed background window and replace it with the main Activity. At this point, the user can start using the app

In the cold start, as a developer, the main parts that can intervene are the OnCreate phase of the Application and the onCreate phase of the Activity, as shown in the following figure:

1.2, hot start

During a warm start, the system puts the activity in the foreground. If all the activities of the application exist in memory, the application can avoid repeated object initialization, rendering, and drawing operations

1.3, warm start

During warm start, since the app process still exists, the second stage of cold start is executed

1. Create an app object

2. Start the main thread

3. Create the main Activity

4. Load the layout

5. Layout the screen

6. Draw the first frame

Common scenarios of warm start:

1. The user restarts the application after exiting the application. The process may continue to run. But the application starts to execute again from calling Activity's onCreate method

2. Insufficient memory, the system will kill the application, and then the user restarts. The process and activity need to be restarted, but the saved instance bundle passed to onCreate is helpful to complete the startup

Next, let's see how to get the startup time~

2. Get the startup time

2.1, use adb command to get

adb shell am start -W [packageName]/[packageName.xxActivity]

adb shell am start -W com.tg.test.gp/com.test.demo.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.tg.test.gp/com.test.demo.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.tg.test.gp/com.test.demo.MainActivity
ThisTime: 1344
TotalTime: 1344
WaitTime: 1346
Complete

ThisTime: The last activity takes time to start

TotalTime: The total time consumed by all activities experienced at startup

WaitTime: The total time for AMS to start all activities (including the time before starting the target application)

2.2, use code to manage

public class LaunchTimer {

    private static long sTime;

    public static void startRecord() {
        sTime = System.currentTimeMillis();
    }
    public static void endRecord() {
        endRecord("");
    }
    public static void endRecord(String msg) {
        long cost = System.currentTimeMillis() - sTime;
        LogUtils.i(msg + "cost " + cost);
    }
}

This method generally sets the start timestamp for the application initialization attachBaseContext method, and the end timestamp is set after the application user's operational interface is fully displayed and operable. The two time difference is the start-up time.

But this method is not elegant. If the startup logic is complicated and there are many methods, it is best to use aop for optimization.

3. Application startup optimization analysis tool

3.1 、 TraceView

Traceview is a performance analysis tool that comes with Android. It can graphically display method call time, call stack, and view all thread information . It is time-consuming to analyze methods, and call chain is a very good tool.

The method of use is to use the code burying method:

1. At the beginning of the collection position, execute Debug.startMethodTracing("app_trace"), where the parameter is the custom file name

2. At the end of the collection position, executeDebug.endMethodTracing()

The file generation path is /sdcard/Android/data/package name/files/file name.trace, the file can be opened using Android Studio

for example:

Let's take a look at the time-consuming method of testAdd

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testAdd(3, 5);
    }

    private void testAdd(int a, int b) {
        Debug.startMethodTracing("app_trace");
        int c = a + b;
        Log.i("Restart", "c =  a + b = " + c);
        Debug.stopMethodTracing();
    }
}

After running the program, find the /sdcard/Android/data/com.restart.perference/files/app_trace.trace file and double-click it in Android Studio to parse out the information in the file. After opening it, it is as follows:

[External link image transfer failed. The source site may have an anti-leech link mechanism. It is recommended to save the image and upload it directly (img-R2s1gpZz-1616421092820)(https://upload-images.jianshu.io/upload_images/25094154-7d9a059e870d906c.image ?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

Let's take a look at how to use CallChart, FlameChart, Top Down, and Bottom Up respectively. First, select the time zone. As in this case, because the program is very simple, the area that can be analyzed is relatively small. After selecting it, you can get the following picture:

image

In the graph obtained by Call Chart , you can see the entire call stack of the program and the time-consuming method.

For example, the testAdd method in the figure has successively called the Debug.startMethodTracing, Log.i, and Debug.stopMethodTracing methods. At the same time, it can be seen from the figure that the startMethodTracing method takes longer than the stopMethodTracing method. In actual optimization, finding which method takes a long time, and targeted optimization is very useful.

When analyzing, we generally cannot optimize third-party programs and system codes. CallChart uses colors very intimately to help us distinguish which codes are written by ourselves. The orange ones are system APIs, the blue ones are generally third-party APIs, and the green ones are written by ourselves. For example, in the testAdd method in the figure, what we wrote ourselves can be adjusted and optimized.

Flame Chart is a reverse chart of Call Chart, with similar functions, the graphics are as follows:

Top Down can see which methods are called inside each method, as well as the time-consuming and time-consuming proportion of each method. Compared with the graphic search of Call Chart, Top Down is more specific and has specific method time-consuming data.

In the figure, Total represents the total time consumed by the method, self represents the time consumed by the non-method calling code in the method, and Children represents the time consumed by other methods called in the method.

Also take the testAdd method as an example. The total time consumed by the testAdd method is 3840us, accounting for 97% of the program running time. The code in the testAdd method takes 342us, accounting for 8.65%, and the other methods called in the testAdd method take a total of 3498us. Accounted for 88.52%

private void testAdd(int a, int b) {
        Debug.startMethodTracing("app_trace");//算到Children中
        int c = a + b;//这一句是算在self耗时中,耗时其实很短
        Log.i("Restart", "c =  a + b = " + c);//算到Children中
        Debug.stopMethodTracing();//算到Children中
}

Bottom Up is the reverse graph of Top Down, you can see which method is called by the method

image

There is another option worth noting in TraceView,

In the upper right corner, there is an option of Wall Clock Time and Thread Time . Wall Clock Time means the actual time consumed by the method, and Thread Time refers to the CPU time. We usually talk about optimization more about optimizing CPU time. When there are IO operations, it is more reasonable to use Thread Time to analyze the time-consuming

In addition, the use of TraceView needs to pay attention to the runtime overhead of TraceView, because it itself takes a long time, which may lead us to optimize the direction.

3.2 、 Systrace

Systrace combines Android kernel data to generate HTML reports

systrace is in the Android/sdk/platform-tools/systrace directory. You need to install python before use, because systrace uses python to generate html

Reported, the command is as follows:

python systrace.py -b 32768 -t 10 -a 包名 -o perference.html sched gfx view wm am app

For specific parameters, please refer to: developer.android.google.cn/studio/prof...

After executing the command, open the report, it shows as follows

Use chrome browser to open, otherwise a white screen may be displayed. If you use chrome to display a white screen, you can enter chrome:tracing in the chrome browser, and then Load the file to display

When viewing the picture, the A key moves to the left, the D key to move to the right, the S key to zoom out, and the W key to zoom in

4. Common optimization

4.1. Common optimization strategies for boot loading

The larger an application, the more modules involved, the more services and even processes it contains, such as network module initialization, low-level data initialization, etc. These loadings need to be prepared in advance, and some unnecessary ones should not be placed in the application. The starting points can usually be sorted from the following four dimensions:

1. Necessary and time-consuming: start initialization, consider using threads to initialize

2. Necessary and not time-consuming: no need to deal with

3. Unnecessarily time-consuming, data reporting, plug-in initialization, and processing on demand

4. Non-essential and time-consuming: just remove it and load it when necessary

The content to be executed when the application is started is classified as described above, and the loading logic is implemented on demand. So what are the common optimized loading strategies?

Asynchronous loading : time-consuming loading is executed asynchronously in the child thread

Lazy loading : Lazy loading of non-essential data

Early loading : use ContentProvider to initialize in advance

The following introduces some common processing of asynchronous loading and delayed loading

4.2, asynchronous loading

Asynchronous loading, in simple terms, is to use sub-threads to load asynchronously. In actual scenarios, various third-party libraries often need to be initialized during startup. By placing the initialization in the child thread, the startup can be greatly accelerated.

But usually, some business logic can only run normally after the initialization of the third-party library. At this time, if you simply put it in a sub-thread to run, it is likely that the business logic will run before the initialization is completed without restrictions. abnormal.

In this more complicated situation, you can use CountDownLatch to deal with, or use the idea of ​​launcher to deal with.

CountDownLatch use

class MyApplication extends Application {

    // 线程等待锁
    private CountDownLatch mCountDownLatch = new CountDownLatch(1);

    // CPU核数
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // 核心线程数
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));

    void onCreate() {
		ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        service.submit(new Runnable() {
            @Override public void run() {
            	//初始化weex,因为Activity加载布局要用到需要提前初始化完成
                initWeex();
                mCountDownLatch.countDown();
            }
        });

        service.submit(new Runnable() {
            @Override public void run() {
            	//初始化Bugly,无需关心是否在界面绘制前初始化完
                initBugly();
            }
        });

        //提交其他库初始化,此处省略。。。

		try {
            //等待weex初始化完才走完onCreate
            mCountDownLatch.await();
		} catch (Exception e) {
            e.printStackTrace();
		}
    }
}

It is recommended to use CountDownLatch when the initialization logic is not complicated. However, if there are interdependencies between the initialized libraries and the logic is complex, it is recommended to use the loader.

Launcher

The core of the launcher is as follows:

  • Make full use of the CPU's multi-core capability, automatically sort out and execute tasks in sequence;
  • The code is Tasked, and the startup task is abstracted into each task;
  • Generate a directed acyclic graph according to all task dependencies sorting;
  • Multithreading is executed in order of thread priority

For specific implementation, please refer to: github.com/NoEndToLF/A...

4.3, lazy loading

The initialization of some third-party libraries is actually not high priority and can be loaded on demand. Or use IdleHandler to initialize in batches when the main thread is idle.

On-demand loading can be implemented according to specific conditions, so I won’t go into details here. Here is an introduction to the use of IdleHandler

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            //当return true时,会移除掉该IdleHandler,不再回调,当为false,则下次主线程空闲时会再次回调
            return false;
        }
    };

Use IdleHandler to do batch initialization, why do you want to batch? When the main thread is idle, the IdleHandler is executed, but if the content of the IdleHandler is too much, it will still cause a freeze. Therefore, it is best to batch the initialization operation when the main thread is idle

public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            //每次执行一个Task,实现分批进行
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            //当为空时,返回false,移除IdleHandler
            return !mDelayTasks.isEmpty();
        }
    };

    //添加初始化任务
    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    //给主线程添加IdleHandler
    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}

4.4, advance loading

The fastest time to initialize in the above scheme is in the onCreate of the Application, but there is an earlier way. The onCreate of the ContentProvider is carried out between the attachBaseContext and onCreate methods of the Application. In other words, it is executed earlier than the onCreate method of Application. So you can use this to load in advance the initialization of third-party libraries.

androidx-startup use

如何使用:
第一步,写一个类实现Initializer,泛型为返回的实例,如果不需要的话,就写Unit
class TimberInitializer : Initializer<Unit> {

    //这里写初始化执行的内容,并返回初始化实例
    override fun create(context: Context) {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
            Timber.d("TimberInitializer is initialized.")
        }
    }

    //这里写初始化的东西依赖的另外的初始化器,没有的时候返回空List
    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }

}

第二步,在AndroidManifest中声明provider,并配置meta-data写初始化的类
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="com.test.pokedex.androidx-startup"
    android:exported=“false"
    //这里写merge是因为其他模块可能也有同样的provider声明,做合并操作
    tools:node="merge">
    //当有相互依赖的情况下,写顶层的初始化器就可以,其依赖的会自动搜索到
    <meta-data
        android:name="com.test.pokedex.initializer.TimberInitializer"
        android:value="androidx.startup" />
</provider>

4.5. Other optimizations

1. In the application, add a startup default image or customize a Theme, and first use a default interface in the Activity to solve the problem of partial startup short black and white screen. Such as android:theme="@style/Theme.AppStartLoad"

5. Summary

1. The main processing of cold start, warm start and hot start and their differences

2. How to get the startup time, introduced the two ways of using adb command and code management

3. How to use tools to find the time-consuming code in the program, and introduce the use of TraceView and Systrace

4. Introduction to common startup optimization methods and implementations (asynchronous loading, delayed loading, early loading, etc.), key ideas: asynchronous, delayed, lazy loading

Guess you like

Origin blog.csdn.net/zhireshini233/article/details/115101211