赞
踩
手机桌面点击一个应用,用户希望应用能 及时响应、快速加载。启动时间过长的应用可能会令用户失望。这种糟糕的体验可能会导致用户在 Play 商店针对您的应用给出很低的评分,甚至完全弃用您的应用。
本篇就来讲解如何分析和优化应用的启动时间。首先介绍启动过程机制,然后讨论如何检测启动时间以及分析工具,最后给出通用启动优化方案。
根据官方文档,应用有三种启动状态:冷启动、温启动、热启动。
冷启动
冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。例如,通过任务列表手动杀掉应用进程后,又重新启动应用。
热启动
热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将您的 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行进程、应用、activity的创建。例如,按home键到桌面,然后又点图标启动应用。
温启动
温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:用户按返回键退出应用后又重新启动应用。这时进程已在运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。
启动优化是在 冷启动 的基础上进行优化。要优化应用以实现快速启动,了解系统和应用层面的情况以及它们在各个状态中的互动方式很有帮助。
在冷启动开始时,系统有三个任务,它们是:
系统一创建应用进程,应用进程就负责后续阶段:
一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口(StartingWindow),替换为主 Activity。此时,用户可以开始使用应用。
详细完整的启动流程分析参考我的文章《Activity的启动过程详解(基于10.0源码)》,这篇从源码角度介绍了 从点击应用图标开始 到添加window后可见 的完整流程。建议阅读理解后再继续此篇启动优化的学习。
下面是官方文档中的启动过程流程图,显示系统进程和应用进程之间如何交接工作。实际上对启动流程的简要概括。
问题来了,启动优化是对 启动流程的那些步骤进行优化呢?
这是一个好问题。我们知道,用户关心的是:点击桌面图标后 要尽快的显示第一个页面,并且能够进行交互。 根据启动流程的分析,显示页面能和用户交互,这是主线程做的事情。那么就要求 我们不能再主线程做耗时的操作。启动中的系统任务我们无法干预,能干预的就是在创建应用和创建 Activity 的过程中可能会出现的性能问题。这一过程具体就是:
activity的onResume方法完成后才开始首帧的绘制。所以这些方法中的耗时操作我们是要极力避免的。
并且,通常情况下,一个应用的主页的数据是需要进行网络请求的,那么用户启动应用是希望快速进入主页以及看到主页数据,这也是我们计算启动结束时间的一个依据。
在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 “Displayed” 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。经过的时间包括以下事件序列:
这是我的demo app 启动的日志打印,查看
2020-07-13 19:54:38.256 18137-18137/com.hfy.androidlearning I/hfy: onResume begin.
2020-07-13 19:54:38.257 18137-18137/com.hfy.androidlearning I/hfy: onResume end.
2020-07-13 19:54:38.269 1797-16782/? I/WindowManager: addWindow: Window{
1402051 u0 com.hfy.androidlearning/com.hfy.demo01.MainActivity}
2020-07-13 19:54:38.391 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s251ms
可见“Displayed”的时间打印是在添加window之后,而添加window是在onResume方法之后。
也可以使用adb命令运行应用来测量初步显示所用时间:
adb shell am start -W [ApplicationId]/[根Activity的全路径]
当ApplicationId和package相同时,根Activity全路径可以省略前面的packageName。
Displayed 指标和前面一样出现在 logcat 输出中:
2020-07-14 14:53:05.294 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s98ms
您的终端窗口在adb命令执行后还应显示以下内容:
hufeiyangdeMacBook-Pro:~ hufeiyang$ adb shell am start -W com.hfy.androidlearning/com.hfy.demo01.MainActivity
Starting: Intent {
act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.hfy.androidlearning/com.hfy.demo01.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.hfy.androidlearning/com.hfy.demo01.MainActivity
TotalTime: 2098
WaitTime: 2100
Complete
我们关注TotalTime即可,即应用的启动时间,包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。
可以使用 reportFullyDrawn() (API19及以上)方法测量从应用启动到完全显示所有资源和视图层次结构所用的时间。什么意思呢?前面核心思想中提到,主页数据请求后完全呈现界面的过程也是一个优化点,而前面的“Displayed”、:“TotalTime”的时间统计都是启动到首帧绘制,那么如何获取 从 启动 到 获取网络请求后再次完成刷新 的时间呢?
要解决此问题,您可以手动调用Activity的 reportFullyDrawn()方法,让系统知道您的 Activity 已完成延迟加载。当您使用此方法时,logcat 显示的值为从创建应用对象到调用 reportFullyDrawn() 时所用的时间。使用示例如下:
@Override protected void onResume() { super.onResume(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } runOnUiThread(new Runnable() { @Override public void run() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { reportFullyDrawn(); } } }); } }).start(); }
使用子线程睡1秒来模拟数据加载,然后调用reportFullyDrawn(),以下是 logcat 的输出。
2020-07-14 15:26:00.979 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s133ms
2020-07-14 15:26:01.788 1797-2017/? I/ActivityTaskManager: Fully drawn com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s943ms
写一个打点工具类,开始结束时分别记录,把时间上报到服务器。
此方法可带到线上,但代码有侵入性。
开始记录的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我们应用能接收到的最早的一个生命周期回调方法。
计算启动结束时间的两种方式
一种是在 onWindowFocusChanged 方法中计算启动耗时。
onWindowFocusChanged 方法只是 Activity 的首帧时间,是 Activity 首次进行绘制的时间,首帧时间和界面完整展示出来还有一段时间差,不能真正代表界面已经展现出来了。
按首帧时间计算启动耗时并不准确,我们要的是用户真正看到我们界面的时间。
正确的计算启动耗时的时机是要等真实的数据展示出来,比如在列表第一项的展示时再计算启动耗时。 (在 Adapter 中记录启动耗时要加一个布尔值变量进行判断,避免 onBindViewHolder 方法被多次调用导致不必要的计算。)
//第一个item 且没有记录过,就结束打点
if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
mHasRecorded = true;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。