Android 启动优化,看过来 ~

1,488 阅读5分钟

Android 的启动有热启动和冷启动,热启动是比较快的,因为热启动是从后台切到前台,应用的 Activity 还驻留在内存中,无需重复执行对象初始化,创建 Applicaiton 和渲染布局等操作,而冷启动经历了创建进程,启动应用和绘制界面等一系列流程,耗时较长,所以,启动优化一般是以冷启动速度为指标的,下面,基于冷启动,分析一下我们能做哪些优化 ?

启动流程

  1. 点击 APP 桌面图标时,Launcher 的 startActivity 方法,通过 Binder 通信,向 system_server 进程中 AMS 服务发起启动请求。
  2. system_server 进程接收到请求后,向 Zygote 进程发送创建进程的请求。
  3. Zygote 进程 fork 出 App 进程,并执行 ActivityThread 的 main 方法,创建 ActivityThread 线程,初始化 Looper,主线程 Handler,同时初始化 ApplicationThread 用于和 AMS 通信交互。
  4. App 进程,通过 Binder 向 sytem_server 进程发起 attachApplication 请求,将 ApplicationThread 对象与 AMS 绑定。
  5. system_server 进程在收到的请求后,进行一些准备工作后,再通过 binder 向 App 进程发送handleBindApplication 和 scheduleLaunchActivity 请求,用于初始化 Application 和创建启动 Activity。
  6. App 进程的在收到请求后,通过 Handler 向主线程发送 BIND_APPLICATION 和 LAUNCH_ACTIVITY 消息,这里是 AMS 和主线程的内部类 ApplicationThread 通过 Binder 通信,ApplicationThread 再和主线程通过 Handler 消息交互。
  7. 主线程在收到消息后,创建 Application 并调用 onCreate 方法,再通过反射机制创建目标 Activity,并回调Activity.onCreate 等方法。
  8. App 正式启动,开始进入 Activity 生命周期,执行完 onCreate,onStart,onResume 方法,UI 渲染后显示 APP 主界面。

image.png

测量方法

adb 命令测量

终端输入 adb shell am start -W packageName/packageName.FirstActivity,就能输出应用的启动时间,但这种方式只适合线下测量。

image.png

TotalTime 表示所有 Activity 启动耗时,WaitTime 表示 AMS 启动 Activity 的总耗时。

埋点测量

使用埋点测量进行用户数据的采集,可以很方便地带到线上,把数据上报给服务器。

定义一个计时器

class StartTimer {

    companion object {
        const val tag = "StartTimer"
        private var time = 0L

        fun startRecord() {
            time = System.currentTimeMillis()
        }

        fun stopRecord() {
            val consume = System.currentTimeMillis() - time
            Log.i(tag, "start timer: $consume")
        }
    }
}

开始记录的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我们应用能接收到的最早的一个生命周期回调方法。

class MyApplication : Application() {

    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        StartTimer.startRecord()
    }
}

停止记录的时机是要等真实的数据展示出来,比如,首屏 Activity 中有个 TextView

val textView: TextView = findViewById(R.id.textView)
textView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
    override fun onPreDraw(): Boolean {
        textView.viewTreeObserver.removeOnPreDrawListener(this)
        StartTimer.stopRecord()
        return false
    }
})

分析工具

得出了启动时间后,我们需要知道哪里比较耗时,这里使用 Debug 和 Android Studio 自带的 Profiler。通过 Debug 的 startMethodTracing 开始跟踪,记录一段时间内的 CPU 使用情况,然后调用 stopMethodTracing 停止跟踪后,这时就会生成一个文件,我们可以通过 Profiler 查看这个文件记录的内容。

举个例子,我们在 MyApplication 的 onCreate 方法里追踪,这里就简单执行一个计算方法。

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        val path = getExternalFilesDir(null)?.path + "/launch.trace"
        Debug.startMethodTracing(path)
        calculate()
        Debug.stopMethodTracing()
    }

    private fun calculate() {
        var x = 0
        for (i in 0..1000) {
            x += i
        }
    }

}

运行之后,我们可以在对应的文件夹下找到 trace 文件,将它拿出来。

image.png

然后我们打开 Profiler,加载这个文件。

image.png

然后就能看到文件记录的内容了,细致到每个方法。

image.png

优化方法

闪屏页

我们知道,APP 启动的时候会创建一个空白的 Window,闪屏页就是利用这个 Window 来显示占位图。这个实际上并没有加快启动速度,只是从用户的感官上进行优化,让用户感觉是快了一点。

比如我们有个 LaunchActivity,作为启动页我们就显示一张图,然后我们同时将这个图片设置到主题上

<style name="launch" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="android:background">@drawable/launch_img</item>
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowNoTitle">true</item>
</style>

然后将其设置到 LaunchActivity 中

<activity
    android:name=".LaunchActivity"
    android:exported="true"
    android:theme="@style/launch">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

效果如下,我们可以看到,空白的 window 已经被启动图占用了,用户感官上相对好些。

hhhhhh.gif

异步初始化

一般我们会在 Application 的 onCreate 里做一些初始化的操作,这些初始化可能每个耗时并不多,但是串行执行,累加起来,总耗时就不一定少了。有些任务不需要主线程执行的,我们可以把它放到子线程上,这样可以加快启动速度。

比如,我们在 Application 中执行初始化操作,这里用线程睡眠来模拟消耗的时间,然后用上面埋点测量的方式得到启动时长。

class MyApplication : Application() {

    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        // 开始记录
        StartTimer.startRecord()
    }

    override fun onCreate() {
        super.onCreate()
        initialization()
    }

    private fun initialization() {
        // 模拟初始化任务耗时
        Thread.sleep(666)
    }
    
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView: TextView = findViewById(R.id.textView)
        textView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                textView.viewTreeObserver.removeOnPreDrawListener(this)
                // 停止记录
                StartTimer.stopRecord()
                return false
            }
        })
    }
}

得到的启动时长如下

image.png

然后我们把这个初始化任务放到子线程中

class MyApplication : Application() {

    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        // 开始记录
        StartTimer.startRecord()
    }

    override fun onCreate() {
        super.onCreate()
        thread {
            initialization()
        }
    }

    private fun initialization() {
        // 模拟初始化任务耗时
        Thread.sleep(666)
    }
    
}

效果还是比较明显的,得到的启动时长如下

image.png

懒加载

对于暂时不会用到的库,可以先不去初始化,等到要用到的时候再去初始化,实现懒加载的目的。

ContentProvider 优化

Application 的启动流程中,会依次执行 Application.attachBaseContext,ContentProvider.onCreate,Application.onCreate。所以,ContentProvider 的 onCreate 方法中,要避免耗时操作,否则会拖慢启动速度。例如,加载数据库是属于耗时操作,我们不应该放在 ContentProvider 的 onCreate 方法中,可以把该操作放到查询中,或者等到启动完成后延时加载。

首屏 Activity 优化

在绘制第一帧之前,主线程会执行 Activity 的 onCreate,onStart,onResume 这三个方法,所以在这三个方法中,主线程不能有耗时操作。