Android startup speed optimization

Welcome to my personal website: https://coderyuan.com

Recently, I have done some optimizations for the startup speed of Android apps. I have some experience and organize them.

The reason that affects the startup speed

Time-consuming tasks

Database initialization, initialization of some third-party frameworks, large file reading, MultiDex loading, etc., cause CPU blocking

Complex View Hierarchy

Too many nested Layouts are used, and the level is deepened, which leads to the recursive deepening of View during the rendering process, which occupies CPU resources and affects the speed of methods such as Measure and Layout.

class is too complex

The creation of Java objects also takes a certain amount of time. If the structure of a class is particularly complex, a new object will consume high resources, especially the initialization of some singletons, and special attention needs to be paid to the structure.

Theme and Activity configuration

Some apps have a Splash page, and some directly enter the main interface. Due to the theme switching, it may cause a white screen, or click the Icon, and the main interface will appear after a while.

Some typical examples and optimization schemes

MultiDex

Since the Dalvik virtual machine used in Android 5.0 and below does not naturally support MultiDex, on the system of 4.4 (and below), if MultiDex is used as the subcontracting scheme, the startup speed may be much slower. The actual value is the same as the dex file. It is related to the size and quantity, it is estimated that it will be 300~500ms slower

  • solution:

    Restrict the use of APPs above 5.0 : At present, most users are already using Android 5.0 and above. Of course, there are many 4.4 users, and many APPs only support 4.4 or above (for example: Baidu APP). For user experience, you can consider giving up some users

    Number of optimization methods : try to avoid more than 65535 methods. At the same time, you can open the Minify option of the Release configuration, and delete the useless methods when packaging. However, if there are many framework references, it is basically ineffective.

    Use less unnecessary frameworks : Some frameworks are very powerful, but not necessarily all of them can be used. Many new methods will be added when they are introduced, which will result in the need to enable MultiDex. You can build your own wheels, or find a lightweight framework.

    Use Kotlin with caution : Since Kotlin is not yet built into the Android system, if the APP uses Kotlin, it may lead to the introduction of many Kotlin methods, resulting in the need to split Dex, which needs to be solved by Google in Android P

    Dex lazy loading : In today's increasingly complex APP functions, MultiDex is almost unavoidable. In order to optimize the startup speed, the necessary methods at startup can be placed in the main Dex (ie classes.dex), the method is in Gradle Configure the multiDexKeepFile or multiDexKeepProguard properties in the script (the code is as follows), see the official documentation for details. After the App is started, use MultiDex.install to load other Dex files. This method is relatively risky, and the implementation cost is relatively high. If there are many libraries that depend on startup, it still cannot be implemented.

    android {
        buildTypes {
            release {
                multiDexKeepFile file('multidex-config.txt')        // multiDexKeepFile规则 
                multiDexKeepProguard file('multidex-config.pro')    // 类似ProGuard的规则
            }
        }
    }

    Configuration file example:

    
    # 常规的multiDexKeepFile规则
    
    
    com/example/MyClass.class
    com/example/MyOtherClass.class
    
    
    # 类似ProGuard规则
    
    
    -keep class com.example.MyClass
    -keep class com.example.MyClassToo
    
    -keep class com.example.** { *; } // All classes in the com.example package

    Plug-in or H5/React Native solution : that is, the terminal only provides the native calling capability and container, and the business is done by the plug-in. Only the basic native capability-related classes need to be loaded locally, and the rest are completely distributed, or built-in resource file calls

Glide and other picture frames

Glide is a very useful image loading framework. In addition to the commonly used image loading and caching functions, Glide supports customization of the network layer, such as replacing OkHttp to support HTTP 2.0. However, in the pursuit of startup speed, when loading a certain image on the Splash page or the main interface, it is often the first time to use Glide. Since Glide is not initialized, it will take a long time to load the image this time (regardless of local Or network), especially when other operations are also occupying CPU resources at the same time, the slowness is particularly obvious! And when you use Glide to load images later, it's still faster.

Time-consuming analysis of Glide initialization: The initialization of Glide will load all configured Modules, then initialize the RequestManager (including the network layer, worker threads, etc., which is time-consuming), and finally apply some decoding options (Options)

Solution: In the onCreate method of Application, call GlideApp.get(this) once in the worker thread

    override fun onCreate() {
        super.onCreate()
        // 使用Anko提供的异步工作协程,或者自行创建一个并发线程池
        doAsync {
            GlideApp.get(this)   // 获取一个Glide对象,Glide内部会进行初始化操作
        }
    }

greenDAO and other database frameworks

greenDAO implements an ORM framework. The database is based on SQLite, which is very convenient to use. There is no need to write SQL statements, control concurrency and transactions, etc. Other common database frameworks such as Realm, DBFlow, etc. are also very convenient to use. However, their initialization, especially when data needs to be upgraded and migrated, often brings a lot of CPU and I/O overhead. Once the amount of data is large (for example: long-term chat records, browser browsing history, etc.) , often requires a dedicated interface to inform users that the APP is doing data processing. Therefore, if you want to improve the startup speed of the APP and avoid the time-consuming task of doing the database when the APP starts, it is necessary!

  • solution:

    Avoid using the database for necessary data: If the display content of the first screen needs to be determined according to the configuration, then simply abandon the database storage and reading, and put it directly in the file and SharedPreference, especially the reading of multiple sets of key-value pairs. If you use a database, After removing the time occupied by initialization, it may take 30~50ms to complete (because it needs to be read multiple times), and if it exists in SharedPreference, even if it is converted to JSON and parsed, it may be within 10ms

    Database pre-asynchronous initialization: When using greenDAO, pre-initialization is necessary to ensure that the main thread resources are not occupied when the database is read for the first time, and the startup speed is prevented from being slowed down. The specific methods are as follows:

    // Application
    override fun onCreate() {
        super.onCreate()
        // 使用Anko提供的异步工作协程,或者自行创建一个并发线程池
        doAsync {
            DbManager.daoSession   // 获取一次greenDao的DaoSession实例化对象即可
        }
    }
    
    // DBManager(数据库相关单例)
    object DbManager {
    
        // greenDAO的DaoMaster,用来初始化数据库并建立连接
        private val daoMaster: DaoMaster by lazy {
            val openHelper = DaoMaster.OpenHelper(ContextUtils.getApplicationContext(), "Test.db")
            DaoMaster(openHelper.writableDatabase)
        }
    
        // 具体的数据库会话
        val daoSession: DaoSession by lazy {
            daoMaster.newSession()
        }
    }

Views and themes

View level

The main reason is that the layout level of the first screen/Splash page is too deep, which causes the recursion to deepen when the View is rendered, consumes too much CPU and memory resources, and blocks the main thread. Therefore, the most fundamental idea is to solve the level problem and check an App's At the View level, you can use the Layout Inspector tool that comes with Android Studio, as shown in the figure:

Layout Inspector

After selecting the process and Window to be checked (Dialog may create a new Window, but the displayed Activity is the same), you can see the content of the Capture automatically performed by Android Studio.

Layout Inspector

According to the content displayed on the left View level, analyze the unnecessary nested layout, and optimize the View level through transformation.

In addition to analyzing the hierarchy according to the above method, you can use Google's latest ConstraintLayout , official website link: ConstraintLayout

The concept of constraint layout adopted by ConstraintLayout is similar to AutoLayout of iOS, but it is far more convenient and powerful to use than AutoLayout. Personally, I feel that it has absorbed the convenience of RelativeLayout, the flexibility of FrameLayout, and the efficiency of LinearLayout. See mutual constraint control through controls. It is possible to build a nearly flat layout, so that the layout hierarchy can be reduced, and only one layer of ConstraintLayout can be used to realize complex UI layout, which is very worth learning and using!

ConstraintLayout ChainStyle

If you use RelativeLayout, FrameLayout, LinearLayout and ConstraintLayout to build a complex layout, perhaps, ConstraintLayout, it will be 50~100ms faster than other layouts!

App Themes

We can do an experiment and use the following themes to see the startup speed of the APP (subject to the Log of ActivityManager):

@android:style/Theme.NoTitleBar.Fullscreen

@android:style/Theme.Black

Default (automatically selected based on operating system)

Among them, the root layout of MainActivity is an empty LinearLayout, which kills the App from cold start 5, and takes the average time

Black

Theme.Black

Average startup time: 160ms

FullScreen

Theme.NoTitleBar.Fullscreen

Average startup time: 126.8ms

default:

default

Average startup time: 174.8ms

A conclusion can be drawn: using a theme without ActionBar is faster, and if even the StatusBar is removed, the speed is the fastest!

The reason is this, when starting an Activity, the system will create a Window that contains a DecorView, and StatusBar and ActionBar are all sub-elements of this View. There is an additional View, and of course, an additional layer of layout. it's definitely time consuming

Therefore, if you want to improve the startup speed of the APP, especially the app using Splash, be sure to set the theme of the first Activity to FullScreen, which can effectively improve the startup speed.

further optimization

Some APPs, such as Weibo, can respond immediately after clicking the icon, and display its Splash page, as shown below:

weibo

And some apps without Splash page will not work, either there is no response after clicking on the desktop icon, and the main interface will appear after a while, or the main interface will appear after a white screen after clicking (on Android 4.4, the problem is particularly obvious due to MultiDex and other problems). )

This is because an APP with a Splash page like Weibo, the Splash page uses the FullScreen theme, and then processes the background of the theme, so that Splash does not load the actual View at all when it starts, but only loads the theme. After the Activity initialization is completed, render the View such as the advertisement space, which avoids the frequent waiting of white screen and blank screen, and makes the user feel that the startup speed is fast. Let's take a look at the changes of View during the startup process of Weibo.

weibo_layout

From the above screen recording GIF, we can see that when Weibo starts, there is no actual View layout, but an entire Layer. After a while, the ImageView layout of Slogan and Logo is gradually loaded in the way of gradual animation. Come out, continue to load the Layout of the ad slot later

The reason for this is very simple, in order to make users feel fast!

apktool look at the apk of Weibo, you can find that Weibo uses a drawable to achieve the theme background of the home page

    <!-- styles.xml -->
    <style name="NormalSplash" parent="@android:style/Theme">
        <item name="android:windowBackground">@drawable/welcome_layler_drawable</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:scrollbarThumbVertical">@drawable/global_scroll_thumb</item>
        <item name="android:windowAnimationStyle">@style/MyAnimationActivity</item>
    </style>
    <!-- welcome_layler_drawable.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <layer-list
    xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:id="@id/welcome_background" android:drawable="@drawable/welcome_android" />
        <item android:bottom="@dimen/login_icon_padding_bottom">
            <bitmap android:gravity="bottom|center" android:src="@drawable/welcome_android_logo" />
        </item>
        <item android:top="@dimen/splash_slogan_margin_top">
            <bitmap android:gravity="center|top" android:src="@drawable/welcome_android_slogan" />
        </item>
        <item android:top="20.0dip" android:right="20.0dip">
            <bitmap android:gravity="center|right|top" android:src="@drawable/channel_logo" />
        </item>
    </layer-list>

It can be seen that using the layer-list form, a series of Bitmaps can be arranged in a form similar to the View layout. By setting the generated drawable as the background form, no View will be generated in the end, which greatly reduces the The time it takes to draw a small View improves the startup speed!

Through experiments, it was found that many APPs on the market (Gode Map, Dianping, Baidu Map, ofo Little Yellow Car, etc.) adopted a similar method, by setting a FullScreen theme Activity, and setting the background to be similar to the Splash layout In the form of , you can click the icon to display the interface immediately

Thinking about multithreading

When the app starts, in order to speed up the startup, multi-threaded means are usually used to execute tasks in parallel, giving full play to the advantages of multi-core CPUs and improving computing efficiency. Although this method can play a certain role in optimizing the startup speed, in actual development, the following points are worth pondering:

What is the appropriate number of concurrent threads? (efficient but not blocking)

Does switching threads frequently have a negative impact? (frequently throwing operations from the main thread into the secondary thread and then throwing the results back would be slower than direct execution)

When is parallel? When is it serial? (Some tasks can only be serialized, and some tasks can be parallelized)

At this time, it is more appropriate to take Android's classic AsyncTask class as an example!

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

The above code is the part where AsyncTask determines the number of thread pools. The core execution pool guarantees a minimum of 2 threads and a maximum of no more than the number of available CPU cores -1, and the maximum number of thread pools is 2 times the number of CPU cores + 1

The purpose of configuring the thread pool in this way is very simple: to prevent excessive concurrency, causing CPU blocking and affecting efficiency

Since Android 3.0, AsyncTask has been changed to serial execution. In fact, it is also to prevent excessive concurrency, causing tasks to snatch CPU time slices, causing blocking, or errors.

This is also to allow tasks with pre- and post-dependencies to be executed in the order we want, so as to control the data flow and prevent inconsistencies, resulting in crashes or data errors

Although AsyncTask is powerful, it often causes problems such as memory leaks due to improper use, and the amount of code is relatively large. Therefore, in the actual use process, a self-encapsulated task queue is generally used, which is lighter, which is convenient for when needed. Let the tasks be executed serially, so as not to cause excessive overhead, so that the speed will not increase but decrease

Here is a piece of implementation code for a lightweight serial queue, which can be referenced when needed:

package com.xiyoumobile.kit.task

import android.os.Handler
import android.os.Looper
import android.os.Message
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue

/**
 * 加入到串行工作队列中,并执行
 */
fun dispatchSerialWork(task: (() -> Unit)): Runnable {
    val runnable = Runnable {
        task()
    }
    addSerialTask(runnable)
    return runnable
}

/**
 * 加入到主线程队列中,并执行
 */
fun dispatchMainLoopWork(task: (() -> Unit)): Runnable {
    val runnable = object : MainTask {
        override fun run() {
            task()
        }
    }
    val msg = Message()
    msg.obj = runnable
    msg.what = runnable.hashCode()
    MAIN_HANDLER.sendMessage(msg)
    return runnable
}

private val BACKGROUND_SERIAL_EXECUTOR = BackgroundSerialExecutor()

private fun addSerialTask(runnable: Runnable) {
    BACKGROUND_SERIAL_EXECUTOR.execute(runnable)
}

private class BackgroundSerialExecutor : Executor {
    private val tasks = LinkedBlockingQueue<Runnable>()
    private val executor = Executors.newSingleThreadExecutor()
    private var active: Runnable? = null

    @Synchronized
    override fun execute(r: Runnable) {
        tasks.offer(Runnable {
            try {
                r.run()
            } finally {
                scheduleNext()
            }
        })
        if (active == null) {
            scheduleNext()
        }
    }

    @Synchronized
    private fun scheduleNext() {
        active = tasks.poll()
        if (active != null) {
            executor.execute(active)
        }
    }
}

private val MAIN_HANDLER = MainLooperHandler()

private class MainLooperHandler : Handler(Looper.getMainLooper()) {

    override fun handleMessage(msg: Message?) {
        super.handleMessage(msg)
        if (msg?.obj is MainTask) {
            (msg.obj as MainTask).run()
        }
    }
}

private interface MainTask : Runnable

Secondly, it can cooperate with Kotlin's coroutines doAsyncto run concurrently in the background. It also uses Executor internally, but based on lighter coroutine operations, the overhead is smaller, and it is suitable for some operations that require concurrent operations, but cannot be used arbitrarily to prevent blocking.

Summarize

Today, with the increasing number of APP functions and the continuous improvement of user experience, the speed of APP startup has become the first threshold that affects user experience. The so-called "fast" is actually a reaction of the user's senses. If the above methods can be used to optimize the startup speed of the APP, although the total operation volume during startup may not be really reduced, after a reasonable sequence arrangement, It can make some unnecessary tasks to be executed after a delay, and play a lighter and more sensitive role when the APP starts, so that it can respond quickly to the user's operation of clicking on the Icon from the Launcher, improve the user experience, and allow users to Feel "fast".

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324844105&siteId=291194637