本文翻译自Google安卓开发文档应用启动时间
了解应用启动内部机制
应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动或热启动。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。建议您始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。
冷启动
冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。这种启动给最大限度地减少启动时间带来了最大的挑战,因为系统和应用要做的工作比在另外两种启动状态中更多。
在冷启动开始时,系统有三个任务,它们是:
- 加载并启动应用
- 在启动后立即显示应用的空白启动窗口
- 创建应用进程
系统一创建应用进程,应用进程就负责后续阶段:
- 创建应用对象
- 启动主线程
- 创建主 Activity
- 视图加载绘制
一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口,替换为主 Activity。此时,用户可以开始使用应用。
图 1 显示系统进程和应用进程之间如何交接工作。
应用创建
当应用启动时,空白启动窗口将保留在屏幕上,直到系统首次完成应用绘制。完成后,系统进程会换掉应用的启动窗口,允许用户开始与应用互动。
如果您在自己的应用中使 Application.onCreate()
过载,系统将在应用对象上调用 onCreate()
方法。之后,应用生成主线程(也称为界面线程),并用其执行创建主 Activity 的任务。
从此时开始,系统级和应用级进程根据应用生命周期阶段继续运行。
Activity 创建
在应用进程创建 Activity 后,Activity 将执行以下操作:
- 初始化值
- 调用构造函数
- 根据 Activity 的当前生命周期状态,相应地调用回调方法,如
Activity.onCreate()
。通常,onCreate()
方法对加载时间的影响最大,因为它执行工作的开销最高:加载和填充视图,以及初始化运行 Activity 所需的对象
热启动
应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将您的 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局填充和呈现。
但是,如果一些内存为响应内存整理事件(如 onTrimMemory()
)而被完全清除,则需要为了响应热启动事件而重新创建相应的对象。
热启动显示的屏幕上行为和冷启动场景相同:在应用完成 Activity 呈现之前,系统进程将显示空白屏幕。
温启动
温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:
- 用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用
onCreate()
从头开始重新创建 Activity。 - 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到
onCreate()
的已保存的实例 state bundle 对于完成此任务有一定助益。
检测和诊断问题工具(Android Vitals)
当您的应用启动时间过长时,Android Vitals 可以通过 Play 管理中心提醒您,从而帮助提升应用性能。Android Vitals 在您的应用出现以下情况时将其启动时间视为过长:
冷启动用了 5 秒或更长时间。
温启动用了 2 秒或更长时间。
热启动用了 1.5 秒或更长时间。
诊断启动时间过长的问题
为了正确诊断启动时间性能,您可以跟踪一些显示应用启动所需时间的指标。
初步显示所用时间
在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 Displayed 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。经过的时间包括以下事件序列:
- 启动进程
- 初始化对象
- 创建并初始化 Activity
- 填充布局
- 首次绘制应用
报告的日志行类似于以下示例:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms
如果您从命令行或在终端中跟踪 logcat 输出,查找经过的时间很简单。要在 Android Studio 中查找经过的时间,必须在 logcat 视图中停用过滤器。停用过滤器是必要的,因为提供此日志的是系统服务器,不是应用本身。
一旦进行了正确的设置,即可轻松搜索正确术语来查看时间。图 2 展示了一个 logcat 输出示例,其中显示了如何停用过滤器,并且在输出内容的倒数第二行中显示了 Displayed 时间。
图 2. 在 logcat 中停用过滤器并查找 Displayed 值。
在所有资源完全加载并显示之前,logcat 输出中的 Displayed 指标不一定会捕获时间:它会省去布局文件中未引用的资源或应用作为对象初始化一部分创建的资源。它之所以排除这些资源是因为加载它们是一个内嵌进程,并且不会阻止应用的初步显示。
有时,logcat 输出中的 Displayed 行中会包含一个总时间的附加字段。例如:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)
在这种情况下,第一个时间测量值仅针对第一个绘制的 Activity。total 时间测量值是从应用进程启动时开始计算,并且可以包含首次启动但未在屏幕上显示任何内容的另一个 Activity。total 时间测量值仅在单个 Activity 的时间和总启动时间之间存在差异时才会显示。
您也可以使用 ADB Shell Activity Manager 命令运行应用来测量初步显示所用时间。示例如下:
adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN
Displayed 指标和以前一样出现在 logcat 输出中。您的终端窗口还应显示以下内容:
Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete
-c 和 -a 为可选参数,可让您为 intent 指定 <category>
和 <action>
。
- WaitTime 表示返回从 startActivity 到应用第一帧完全显示这段时间. 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;
- ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
- TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用Activity pause的耗时。
开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。
完全显示所用时间
您可以使用 reportFullyDrawn() 方法测量从应用启动到完全显示所有资源和视图层次结构所用的时间。在应用执行延迟加载时,此数据会很有用。在延迟加载中,应用不会阻止窗口的初步绘制,但会异步加载资源并更新视图层次结构。
如果由于延迟加载,应用的初步显示不包括所有资源,您可能会将完全加载和显示所有资源及视图视为单独的指标:例如,您的界面可能已完全加载,并绘制了一些文本,但尚未显示应用必须从网络中获取的图片。
要解决此问题,您可以手动调用 reportFullyDrawn(),让系统知道您的 Activity 已完成延迟加载。当您使用此方法时,logcat 显示的值为从创建应用对象到调用 reportFullyDrawn() 时所用的时间。以下是 logcat 输出的示例:
system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms
logcat 输出有时包含 total 时间,如初步显示所用时间中所述。
注意常见问题
本节讨论几个通常会影响应用启动性能的问题。这些问题主要涉及初始化应用和 Activity 对象,以及屏幕加载。
密集型应用初始化
在您的代码替换 Application 对象,并在初始化该对象过程中执行密集工作或复杂逻辑时,启动性能可能会受影响。如果您的应用子类执行尚不需要完成的初始化,则您的应用可能会在启动过程中浪费时间。有些初始化可能完全没有必要:例如,当应用为了响应 intent 而实际上已经启动时,初始化主 Activity 的状态信息就是不必要的。通过 intent,应用仅使用之前初始化状态数据的一个子集。
应用初始化过程中的其他挑战包括影响范围较大或数量众多的垃圾回收事件,或与初始化同时发生、会进一步阻止初始化过程的磁盘 I/O。垃圾回收是 Dalvik 运行时特别需要考虑的问题;Art 运行时同时执行垃圾回收,从而最大限度地减少该操作的影响。
方法跟踪记录
运行 CPU 性能剖析器显示,callApplicationOnCreate() 方法最终调用您的 com.example.customApplication.onCreate 方法。如果该工具显示这些方法需要很长时间才能完成执行,您应该进一步探索以查看正在进行哪些工作。
内嵌跟踪记录
使用内嵌跟踪记录调查可能的问题根源,包括:
- 应用的初始
onCreate()
函数 - 应用初始化的任何全局单例对象
- 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环
问题解决方案
不管问题在于不必要的初始化还是磁盘 I/O,解决方案都会调用延迟初始化对象:仅初始化立即需要的对象。例如,不创建全局静态对象,而是转为单例模式,其中应用仅在第一次访问对象时初始化它们。此外,考虑使用依赖注入框架(如 Dagger
),它们会在首次注入时创建对象和依赖项。
密集型 Activity 初始化
创建 Activity 通常需要进行大量的高开销工作。通常有机会优化这项工作以实现性能改进。此类常见问题包括:
- 加载大型或复杂的布局
- 阻止磁盘上的屏幕绘制或网络 I/O
- 加载和解码位图
- 栅格化 VectorDrawable 对象
- 初始化 Activity 的其他子系统
诊断问题
事实证明,在这种情况下,方法跟踪记录和内嵌跟踪记录同样很有用。
方法跟踪记录
使用 CPU Profiler 时,请注意应用的 Application 子类构造函数和 com.example.customApplication.onCreate() 方法。
如果该工具显示这些方法需要很长时间才能完成执行,您应该进一步探索以查看正在进行哪些工作。
内嵌跟踪记录
使用内嵌跟踪记录调查可能的问题根源,包括:
- 应用的初始 onCreate() 函数
- 应用初始化的任何全局单例对象
- 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环
优化方案
潜在瓶颈有很多,但两种常见问题和补救措施如下所示:
- 您的视图层次结构越大,应用膨胀它所花的时间就越长。解决此问题的两个步骤是:
- 通过减少冗余或嵌套布局,展平您的视图层次结构。
- 不要加载在启动期间无需显示的界面部分,而是使用
ViewStub
对象作为应用可以在更合适的时间加载的子层次结构的占位符。
- 在主线程上进行所有资源初始化也会降低启动速度。您可以按以下方式解决此问题:
- 转移所有资源初始化,以便应用可以在其他线程上延迟执行。
- 允许应用加载并显示您的视图,稍后再更新依赖于位图和其他资源的可视属性。
带主题背景的启动屏幕
您可能希望为应用的加载体验设置主题背景,从而使应用的启动屏幕在主题背景上与应用的其余部分保持一致,而不是与系统主题背景一致。这样做可以隐藏缓慢的 Activity 启动。
实现带主题背景的启动屏幕的常见方式是使用 windowDisablePreview
主题背景属性来关闭启用应用时系统进程绘制的初始空白屏幕。但是,此方法可能导致启动时间比不抑制预览窗口的应用更长。此外,它还会使用户在 Activity 启动过程中只能等待而不会收到任何反馈,这会让用户无法确定应用是否在正常运行。
诊断问题
通常可以用于诊断此问题的方法是观察用户启动应用时,应用的响应是否很慢:在这种情况下,屏幕看起来会像是卡住了,或停止了对输入做出响应。
问题解决方案
建议遵守常见的 Material Design
模式,而不是停用预览窗口。您可以使用 Activity 的 windowBackground
主题背景属性,为启动 Activity 提供简单的自定义可绘制对象。
例如,您可以创建新的可绘制文件,并从布局 XML 和应用清单文件中引用它,如下所示:
布局 XML 文件:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/white"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/product_logo_144dp"
android:gravity="center"/>
</item>
</layer-list>
清单文件:
<activity ...
android:theme="@style/AppTheme.Launcher" />
要切回到正常主题背景,最简单的方式是先调用 setTheme(R.style.AppTheme)
,然后再调用 super.onCreate()
和 setContentView()
:
public class MyMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Make sure this is before calling super.onCreate
setTheme(R.style.Theme_MyApp);
super.onCreate(savedInstanceState);
// ...
}
}
线上启动时间的统计方式
起始时间点
起始时间点比较容易记录:如果记录冷启动启动时间一般可以在 Application.attachBaseContext()
开始的位置记录起始时间点,因为在这之前 Context
还没有初始化,一般也干不了什么事情,当然这个是要视具体情况来定,其实只要保证在 App 的具体业务逻辑开始执行之前记录起始时间点即可。如果记录热启动启动时间点可以在 Activity.onRestart()
中记录起始时间点。
结束时间点
结束时间点理论上要选在 App 显示出第一屏界面的时候,但是在什么位置 App 显示出第一屏界面呢?网上很多文章说在 Activity 的 onResume
方法执行完成之后,Activity 就对用户可见了,实际上并不是,一个 Activity 走完onCreate onStart onResume
这几个生命周期之后,只是完成了应用自身的一些配置,比如 Activity 主题设置 window 属性的设置 View 树的建立,但是其实后面还需要各个 View 执行 measure layout draw
等。所以在 onResume
中记录结束时间点的 Log 并不准确,大家可以注意一下上面流程中最后一个函数 Activity.onWindowFocusChanged
,这个函数是判断 activity 是否可见的最佳位置,所以我们可以在 Activity.onWindowFocusChanged
记录应用启动的结束时间点,不过需要注意的是该函数,在 Activity 焦点发生变化时就会触发,所以要做好判断,去掉不需要的情况。