1.概述
手机点击一个APP,用户希望应用能够及时响应并快速加载。启动时间过长的应用不能满足这个期望,并且可能会令用户失望。这种糟糕的体验可能会导致用户在 Play 商店针对您的应用给出很低的评分,甚至完全弃用您的应用。
本篇就来将帮助您分析和优化应用的启动时间;首先介绍启动过程的内部机制;然后,讨论如何剖析启动性能(检测启动时间以及分析工具),最后给出通用启动优化方案;
2.了解应用启动内部机制
应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动或热启动。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。建议您始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。
- 冷启动 冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。这种启动给最大限度地减少启动时间带来了最大的挑战,因为系统和应用要做的工作比在另外两种启动状态中更多。
- 热启动 应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将您的 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局膨胀和呈现。例如,按home键到桌面,然后又点图标启动应用。
-
温启动 温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:用户按返回键退出应用后又重新启动应用。这时进程已在运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。
始终在假定冷启动的基础上进行优化,要优化应用以实现快速启动,了解系统和应用层面的情况以及它们在各个状态中的互动方式很有帮助。
在冷启动开始时,系统有三个任务,它们是:
- 加载并启动应用。
- 在启动后立即显示应用的空白启动窗口。
- 创建应用进程
系统一创建应用进程,应用进程就负责后续阶段:
- 创建应用对象。
- 启动主线程。
- 创建主 Activity。
- 扩充视图。
- 布局屏幕。
- 执行初始绘制。
一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口,替换为主 Activity。此时,用户可以开始使用应用。
显示系统进程和应用进程之间如何交接工作如下图:
上图实际上对启动流程的简要概括;
3.优化核心思想
问题来了,启动优化是对 启动流程的那些步骤进行优化呢?
这是一个好问题。我们知道,用户关心的是:点击桌面图标后 要尽快的显示第一个页面,并且能够进行交互。 根据启动流程的分析,显示页面能和用户交互,这是主线程做的事情。那么就要求 我们不能再主线程做耗时的操作。启动中的系统任务我们无法干预,能干预的就是在创建应用和创建 Activity 的过程中可能会出现的性能问题。这一过程具体就是:
- Application的attachBaseContext
- Application的onCreate
- activity的onCreate
- activity的onStart
- activity的onResume
activity的onResume方法完成后才开始首帧的绘制。所以这些方法中的耗时操作我们是要极力避免的。
并且,通常情况下,一个应用的主页的数据是需要进行网络请求的,那么用户启动应用是希望快速进入主页以及看到主页数据,这也是我们计算启动结束时间的一个依据。
4.时间检测
4.1Displayed
为了正确诊断启动时间性能,您可以跟踪一些显示应用启动所需时间的指标;
在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 Displayed
的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。经过的时间包括以下事件序列:
- 启动进程。
- 初始化对象。
- 创建并初始化 Activity。
- 扩充布局。
- 首次绘制应用。
我们APP启动报告的日志打印似于以下示例:
2021-12-05 18:30:21.559 7678-7678/com.XXX.XXX D/GOA_APP:: onResume begin
2021-12-05 18:30:21.559 7678-7678/com.XXX.XXX D/GOA_APP:: onResume 显示第一帧
2021-12-05 18:30:21.559 7678-7678/com.XXX.XXX D/GOA_APP:: onResume end2021-12-05 18:30:23.932 1558-1637/? D/SmartisanLaunch: cold launch: package:com.XXX.XXX activity:com.XXX.XXX/.ui.activity.LauncherActivity start_time:931490 end_time:941368 duration:9878ms
2021-12-05 18:30:23.932 1558-1637/? I/ActivityManager: Displayed com.XXX.XXX/.ui.activity.LauncherActivity: +9s809ms
“Displayed”的时间打印是在添加window之后,而添加window是在onResume方法之后;冷启动(cold launch)时间记录及消耗总时间;
如果您从命令行或在终端中跟踪 logcat 输出,查找经过的时间很简单。要在 Android Studio 中查找经过的时间,必须在 logcat 视图中停用过滤器。停用过滤器是必要的,因为提供此日志的是系统服务器,不是应用本身。
一旦进行了正确的设置,即可轻松搜索正确术语来查看时间。下图展示了一个 logcat 输出示例,其中显示了如何停用过滤器,并且在输出内容的倒数第二行中显示了 Displayed
时间;
也可以查看其它页面或者APP启动时间,例如今日头条
2021-12-06 10:31:24.366 1559-1602/? I/ActivityManager: Displayed com.ss.android.article.news/.activity.MainActivity: +2s199ms
4.2adb shell
您也可以使用 ADB Shell Activity Manager 命令运行应用来测量初步显示所用时间:
adb [-d|-e|-s <serialNumber>] shell am start -S -W [ApplicationId]/[根Activity的全路径] -c android.intent.category.LAUNCHER -a android.intent.action.MAIN 当ApplicationId和package相同时,根Activity全路径可以省略前面的packageName。
adb命令使用参考:Android 调试桥 (adb)
Displayed
指标和以前一样出现在 logcat 输出中。您的终端窗口还应显示以下内容:
2021-12-05 19:02:46.937 1558-1637/? I/ActivityManager: Displayed com.XXX.XXX/.ui.activity.LauncherActivity: +1s827ms
您的终端窗口在adb命令执行后还应显示以下内容:
C:\Users\dongdawei1>adb shell am start -W com.XXX.XXX/.ui.activity.LauncherActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.XXX.XXX/.ui.activity.LauncherActivity }
Status: ok
Activity: com.XXX.XXX/.ui.activity.LauncherActivity
ThisTime: 1827
TotalTime: 1827
WaitTime: 1859
Complete
我们关注TotalTime即可,即应用的启动时间,包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程;
4.3reportFullyDrawn()
您可以使用 reportFullyDrawn() (API19及以上)方法测量从应用启动到完全显示所有资源和视图层次结构所用的时间。在应用执行延迟加载时,此数据会很有用。在延迟加载中,应用不会阻止窗口的初步绘制,但会异步加载资源并更新视图层次结构。
如果由于延迟加载,应用的初步显示不包括所有资源,您可能会将完全加载和显示所有资源及视图视为单独的指标:例如,您的界面可能已完全加载,并绘制了一些文本,但尚未显示应用必须从网络中获取的图片。 要解决此问题,您可以手动调用 reportFullyDrawn(),让系统知道您的 Activity 已完成延迟加载。当您使用此方法时,logcat 显示的值为从创建应用对象到调用 reportFullyDrawn() 时所用的时间。以下是 logcat 输出的示例:
Activity @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 的输出;
2021-12-05 19:11:05.045 1558-1637/? I/ActivityManager: Displayed com.XXX.XXX/.ui.activity.LauncherActivity: +1s842ms
2021-12-05 19:11:05.824 1558-2747/? I/ActivityManager: Fully drawn com.XXX.XXX/.ui.activity.LauncherActivity: +2s622ms
4.4代码打点
写一个打点工具类,开始结束时分别记录,把时间上报到服务器;
此方法可带到线上,但代码有侵入性;
开始记录的位置放在Application的attachBaseContext方法中,attachBaseContext是我们应用能接收到的最早一个生命周期回调方法;
计算启动结束时间的两种方式
- 一种是在 onWindowFocusChanged 方法中计算启动耗时。 onWindowFocusChanged 方法只是 Activity 的首帧时间,是 Activity 首次进行绘制的时间,首帧时间和界面