Android 的启动有热启动和冷启动,热启动是比较快的,因为热启动是从后台切到前台,应用的 Activity 还驻留在内存中,无需重复执行对象初始化,创建 Applicaiton 和渲染布局等操作,而冷启动经历了创建进程,启动应用和绘制界面等一系列流程,耗时较长,所以,启动优化一般是以冷启动速度为指标的,下面,基于冷启动,分析一下我们能做哪些优化 ?
启动流程
- 点击 APP 桌面图标时,Launcher 的 startActivity 方法,通过 Binder 通信,向 system_server 进程中 AMS 服务发起启动请求。
- system_server 进程接收到请求后,向 Zygote 进程发送创建进程的请求。
- Zygote 进程 fork 出 App 进程,并执行 ActivityThread 的 main 方法,创建 ActivityThread 线程,初始化 Looper,主线程 Handler,同时初始化 ApplicationThread 用于和 AMS 通信交互。
- App 进程,通过 Binder 向 sytem_server 进程发起 attachApplication 请求,将 ApplicationThread 对象与 AMS 绑定。
- system_server 进程在收到的请求后,进行一些准备工作后,再通过 binder 向 App 进程发送handleBindApplication 和 scheduleLaunchActivity 请求,用于初始化 Application 和创建启动 Activity。
- App 进程的在收到请求后,通过 Handler 向主线程发送 BIND_APPLICATION 和 LAUNCH_ACTIVITY 消息,这里是 AMS 和主线程的内部类 ApplicationThread 通过 Binder 通信,ApplicationThread 再和主线程通过 Handler 消息交互。
- 主线程在收到消息后,创建 Application 并调用 onCreate 方法,再通过反射机制创建目标 Activity,并回调Activity.onCreate 等方法。
- App 正式启动,开始进入 Activity 生命周期,执行完 onCreate,onStart,onResume 方法,UI 渲染后显示 APP 主界面。
测量方法
adb 命令测量
终端输入 adb shell am start -W packageName/packageName.FirstActivity,就能输出应用的启动时间,但这种方式只适合线下测量。
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 文件,将它拿出来。
然后我们打开 Profiler,加载这个文件。
然后就能看到文件记录的内容了,细致到每个方法。
优化方法
闪屏页
我们知道,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 已经被启动图占用了,用户感官上相对好些。
异步初始化
一般我们会在 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
}
})
}
}
得到的启动时长如下
然后我们把这个初始化任务放到子线程中
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)
}
}
效果还是比较明显的,得到的启动时长如下
懒加载
对于暂时不会用到的库,可以先不去初始化,等到要用到的时候再去初始化,实现懒加载的目的。
ContentProvider 优化
Application 的启动流程中,会依次执行 Application.attachBaseContext,ContentProvider.onCreate,Application.onCreate。所以,ContentProvider 的 onCreate 方法中,要避免耗时操作,否则会拖慢启动速度。例如,加载数据库是属于耗时操作,我们不应该放在 ContentProvider 的 onCreate 方法中,可以把该操作放到查询中,或者等到启动完成后延时加载。
首屏 Activity 优化
在绘制第一帧之前,主线程会执行 Activity 的 onCreate,onStart,onResume 这三个方法,所以在这三个方法中,主线程不能有耗时操作。