用户期望应用加载速度快且响应迅速。启动时间慢的应用无法满足此期望,可能会让用户失望。这种糟糕的体验会导致用户在 Play 商店中对您的应用评分较低,甚至完全放弃您的应用。
此页面提供信息,帮助您优化应用的启动时间,包括启动过程内部机制的概述、如何分析启动性能以及一些常见的启动时间问题以及如何解决这些问题的技巧。
了解不同的应用启动状态
应用启动可以处于三种状态之一:冷启动、温启动或热启动。每种状态都会影响应用对用户可见所需的时间。在冷启动中,您的应用将从头开始。在其他状态下,系统需要将正在运行的应用从后台带到前台。
我们建议您始终基于冷启动的假设进行优化。这样做也可以提高温启动和热启动的性能。
为了优化应用以实现快速启动,了解系统和应用级别发生的情况以及它们在每种状态下的交互方式非常有用。
确定应用启动时间的两个重要指标是 首次显示时间 (TTID) 和 完全绘制时间 (TTFD)。TTID 是显示第一帧所需的时间,而 TTFD 是应用完全可交互所需的时间。两者都同等重要,因为 TTID 让用户知道应用正在加载,而 TTFD 是应用实际可用时的时间。如果这两个时间过长,用户可能会在应用完全加载之前退出。
冷启动
冷启动是指应用从头开始启动。这意味着在此启动之前,系统进程会创建应用的进程。冷启动发生在以下情况下:例如,自设备启动以来或自系统终止应用以来,您的应用首次启动。
这种类型的启动对最小化启动时间提出了最大的挑战,因为系统和应用需要做的工作比其他启动状态多。
冷启动开始时,系统有以下三个任务:
- 加载并启动应用。
- 在启动后立即为应用显示一个空白的启动窗口。
- 创建应用进程。
系统创建应用进程后,应用进程负责后续阶段:
- 创建应用对象。
- 启动主线程。
- 创建主活动。
- 填充视图。
- 布局屏幕。
- 执行初始绘制。
当应用进程完成第一次绘制时,系统进程会替换显示的背景窗口,将其替换为主活动。此时,用户可以开始使用应用。
图1显示了系统和应用进程如何在彼此之间传递工作。
在创建应用和创建活动期间可能会出现性能问题。
应用创建
当您的应用启动时,空白的启动窗口会保留在屏幕上,直到系统第一次完成应用的绘制。此时,系统进程会将启动窗口替换为您的应用,允许用户与应用交互。
如果您在自己的应用中重写了Application.onCreate()
,则系统会在您的应用对象上调用onCreate()
方法。之后,应用会生成主线程(也称为UI线程),并将其分配创建您的主活动的任务。
从这一点开始,系统级和应用级进程将根据应用生命周期阶段进行。
活动创建
应用进程创建您的活动后,活动将执行以下操作:
- 初始化值。
- 调用构造函数。
- 调用回调方法,例如
Activity.onCreate()
,这与活动的当前生命周期状态相符。
通常,onCreate()
方法对加载时间的影响最大,因为它执行开销最高的工作:加载和填充视图以及初始化活动运行所需的项。
温启动
温启动包含冷启动过程中发生的部分操作。同时,它比热启动的开销更大。许多潜在状态都可以视为温启动,例如:
用户退出您的应用,然后重新启动它。进程可能会继续运行,但应用必须使用对
onCreate()
的调用从头开始重新创建活动。系统将您的应用从内存中逐出,然后用户重新启动它。进程和活动需要重新启动,但任务可以从传递到
onCreate()
的已保存实例状态包中获益。
热启动
您的应用的热启动比冷启动的开销更低。在热启动中,系统将您的活动置于前台。如果您的所有应用活动仍在内存中,则应用可以避免重复对象初始化、布局填充和渲染。
但是,如果响应内存调整事件(例如onTrimMemory()
)而清除了一些内存,则需要重新创建这些对象以响应热启动事件。
热启动显示与冷启动场景相同的屏幕行为。系统进程会显示一个空白屏幕,直到应用完成活动的渲染。
如何在Perfetto中识别应用启动
要调试应用启动问题,确定应用启动阶段中包含的内容非常有用。要在Perfetto中识别整个应用启动阶段,请按照以下步骤操作:
在Perfetto中,找到包含Android应用启动派生指标的行。如果您没有看到它,请尝试使用设备上的系统跟踪应用捕获跟踪。
单击关联的切片并按m选择切片。括号会出现在切片周围,并表示它花费了多长时间。持续时间也会显示在“当前选择”选项卡中。
通过单击别针图标来固定Android应用启动行,当您将指针悬停在该行上时,该图标会可见。
滚动到包含相关应用的行,然后单击第一个单元格以展开该行。
通过按w放大主线程(通常位于顶部)(分别按s, a, d缩小、左移和右移)。
派生指标切片使更容易看到应用启动中包含的内容,因此您可以继续更详细地进行调试。
使用指标检查和改进启动
为了正确诊断启动时间性能,您可以跟踪显示应用启动所需时间的指标。Android 提供了几种方法来向您显示您的应用存在问题并帮助您诊断它。Android vitals可以提醒您出现问题,诊断工具可以帮助您诊断问题。
利用启动指标的好处
Android 使用首次显示时间 (TTID)和完全显示时间 (TTFD)指标来优化冷启动和温启动应用。Android运行时 (ART) 使用这些指标中的数据来有效地预编译代码,以优化未来的启动。
更快的启动速度会导致用户与您的应用进行更持久的交互,从而减少提前退出、重新启动实例或导航到其他应用的情况。
Android 指标
当您的应用启动时间过长时,Android vitals 可以通过在Play Console上向您发出警报来帮助提高您的应用性能。
Android vitals 认为您的应用以下启动时间过长:
Android vitals 使用首次显示时间 (TTID)指标。有关 Google Play 如何收集 Android vitals 数据的信息,请参阅Play Console 文档。
首次显示时间
首次显示时间 (TTID) 是显示应用 UI 的第一帧所需的时间。此指标衡量应用生成第一帧所需的时间,包括冷启动期间的进程初始化、冷启动或温启动期间的活动创建以及显示第一帧。保持应用的 TTID 较低有助于通过让用户快速看到应用启动来改善用户体验。TTID 由 Android 框架自动为每个应用报告。在优化应用启动时,我们建议实现reportFullyDrawn
以获取更多信息,直至TTFD。
TTID 衡量的是一个时间值,它表示总的经过时间,其中包括以下事件序列:
- 启动进程。
- 初始化对象。
- 创建和初始化活动。
- 填充布局。
- 第一次绘制应用。
检索 TTID
要查找 TTID,请在Logcat 命令行工具中搜索包含名为Displayed
的值的输出行。此值是 TTID,类似于以下示例,其中 TTID 为 3s534ms:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms
要在 Android Studio 中查找 TTID,请从过滤器下拉菜单中禁用 Logcat 视图中的过滤器,然后查找Displayed
时间,如图 5 所示。禁用过滤器是必要的,因为系统服务器(而不是应用本身)提供此日志。
Logcat 输出中的Displayed
指标并不一定能捕获直到加载和显示所有资源为止的时间量。它省略了在布局文件中未引用的资源或应用作为对象初始化的一部分创建的资源。它排除了这些资源,因为加载它们是一个内联过程,并且不会阻塞应用的初始显示。
有时,Logcat 输出中的Displayed
行包含总时间的附加字段。例如:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)
在这种情况下,第一次测量仅针对第一次绘制的活动。 total
时间测量从应用进程启动开始,可能包含另一个首先启动但未向屏幕显示任何内容的活动。total
时间测量仅在单个活动和总启动时间之间存在差异时显示。
我们建议在 Android Studio 中使用 Logcat,但如果您不使用 Android Studio,也可以通过使用adb
shell activity manager 命令运行应用来测量 TTID。这是一个示例:
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
参数是可选的,允许您指定<category>
和<action>
。
完全显示时间
完全显示时间 (TTFD) 是应用对用户变得可交互所需的时间。它报告为显示应用 UI 的第一帧以及在显示初始帧后异步加载的内容所需的时间。通常,这是从网络或磁盘加载的主要内容,由应用报告。换句话说,TTFD 包括 TTID 以及应用变得可用所需的时间。保持应用的 TTFD 较低有助于通过让用户快速与您的应用交互来改善用户体验。
当Choreographer
调用活动的onDraw()
方法时,系统会确定 TTID,并且当它知道第一次调用它时。但是,系统不知道何时确定 TTFD,因为每个应用的行为都不同。要确定 TTFD,应用需要向系统发出信号以告知其何时达到完全绘制状态。
检索 TTFD
要查找TTFD,请通过调用reportFullyDrawn()
方法(位于ComponentActivity
类中)来发出已完全绘制状态的信号。 reportFullyDrawn
方法报告应用程序何时完全绘制并处于可用状态。TTFD 是从系统接收应用程序启动意图到调用 reportFullyDrawn()
所经过的时间。如果您不调用 reportFullyDrawn()
,则不会报告任何 TTFD 值。
要测量 TTFD,请在完全绘制 UI 和所有数据后调用 reportFullyDrawn()
。请勿在系统测量的第一个活动窗口首次绘制和显示之前调用 reportFullyDrawn()
,因为那样系统会报告系统测量的时长。换句话说,如果您在系统检测到 TTID 之前调用 reportFullyDrawn()
,则系统会将 TTID 和 TTFD 报告为相同的值,而此值即为 TTID 值。
当您使用 reportFullyDrawn()
时,Logcat 会显示如下所示的输出,其中 TTFD 为 1 秒 54 毫秒。
system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms
Logcat 输出有时会包含total
时间,如初始显示时间中所述。
如果您的显示时间比您期望的慢,您可以尝试找出启动过程中的瓶颈。
在您知道已达到完全绘制状态的基本情况下,您可以使用 reportFullyDrawn()
来发出完全绘制状态的信号。但是,在后台线程必须完成后台工作才能达到完全绘制状态的情况下,您需要延迟 reportFullyDrawn()
以获得更准确的 TTFD 测量值。要了解如何延迟 reportFullyDrawn()
,请参阅下一节。
提高启动时间的准确性
如果您的应用正在执行延迟加载,并且初始显示不包含所有资源(例如,当您的应用正在从网络获取图像时),您可能希望延迟调用 reportFullyDrawn
,直到您的应用可用为止,以便您可以将列表填充包含在基准测试时间内。
例如,如果 UI 包含动态列表(例如 RecyclerView
或延迟加载列表),则这可能由在列表首次绘制后(因此在 UI 被标记为完全绘制后)完成的后台任务填充。在这种情况下,列表填充不包含在基准测试中。
要将列表填充包含在基准测试时间内,请使用 getFullyDrawnReporter()
获取 FullyDrawnReporter
,并在您的应用代码中添加一个报告器。后台任务完成填充列表后,释放报告器。
FullyDrawnReporter
只有在所有添加的报告器都已释放后才会调用 reportFullyDrawn()
方法。通过在后台进程完成之前添加报告器,计时还包括将列表填充到启动时间数据中所需的时间。这不会更改用户对应用的行为,但它使启动时间数据包含填充列表所需的时间。reportFullyDrawn()
直到所有任务都完成后才会调用,与顺序无关。
以下示例显示了如何同时运行多个后台任务,每个任务都注册自己的报告器。
Kotlin
class MainActivity : ComponentActivity() { sealed interface ActivityState { data object LOADING : ActivityState data object LOADED : ActivityState } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { var activityState by remember { mutableStateOf(ActivityState.LOADING as ActivityState) } fullyDrawnReporter.addOnReportDrawnListener { activityState = ActivityState.LOADED } ReportFullyDrawnTheme { when(activityState) { is ActivityState.LOADING -> { // Display the loading UI. } is ActivityState.LOADED -> { // Display the full UI. } } } SideEffect { lifecycleScope.launch(Dispatchers.IO) { fullyDrawnReporter.addReporter() // Perform the background operation. fullyDrawnReporter.removeReporter() } lifecycleScope.launch(Dispatchers.IO) { fullyDrawnReporter.addReporter() // Perform the background operation. fullyDrawnReporter.removeReporter() } } } } }
Java
public class MainActivity extends ComponentActivity { private FullyDrawnReporter fullyDrawnReporter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); fullyDrawnReporter = getFullyDrawnReporter(); fullyDrawnReporter.addOnReportDrawnListener(() -> { // Trigger the UI update. return Unit.INSTANCE; }); new Thread(new Runnable() { @Override public void run() { fullyDrawnReporter.addReporter(); // Do the background work. fullyDrawnReporter.removeReporter(); } }).start(); new Thread(new Runnable() { @Override public void run() { fullyDrawnReporter.addReporter(); // Do the background work. fullyDrawnReporter.removeReporter(); } }).start(); } }
如果您的应用使用 Jetpack Compose,您可以使用以下 API 来指示完全绘制状态。
ReportDrawn
:指示您的可组合项已立即准备好进行交互。ReportDrawnWhen
:采用一个谓词(例如list.count > 0
)来指示您的可组合项何时准备好进行交互。ReportDrawnAfter
:采用一个挂起方法,该方法完成后会指示您的可组合项已准备好进行交互。
识别瓶颈
要查找瓶颈,您可以使用 Android Studio CPU Profiler。有关更多信息,请参阅使用 CPU Profiler 检查 CPU 活动。
您还可以通过应用和活动的 onCreate()
方法内的内联跟踪来深入了解潜在的瓶颈。要了解内联跟踪,请参阅Trace
函数的文档和系统跟踪概述。
解决常见问题
本节讨论一些经常影响应用启动性能的问题。这些问题主要涉及初始化应用和活动对象以及加载屏幕。
繁重的应用初始化
当您的代码覆盖 Application
对象并在初始化该对象时执行繁重的工作或复杂的逻辑时,启动性能可能会受到影响。如果您的 Application
子类执行尚不需要完成的初始化,则您的应用可能会在启动期间浪费时间。
某些初始化可能完全没有必要,例如,当应用实际上是响应意图启动时初始化主活动的状态信息。使用意图时,应用仅使用先前初始化的状态数据的子集。
应用初始化期间的其他挑战包括影响大或数量众多的垃圾回收事件,或者与初始化同时发生的磁盘 I/O,这进一步阻塞了初始化过程。垃圾回收尤其需要考虑 Dalvik 运行时;Android 运行时 (ART) 并发执行垃圾回收,从而最大限度地减少该操作的影响。
诊断问题
您可以使用方法跟踪或内联跟踪来尝试诊断问题。
方法跟踪
运行 CPU Profiler 显示 callApplicationOnCreate()
方法最终会调用您的 com.example.customApplication.onCreate
方法。如果工具显示这些方法需要很长时间才能完成执行,请进一步探究以查看在那里正在执行哪些工作。
内联跟踪
使用内联跟踪来调查可能的罪魁祸首,包括以下内容:
- 您应用的初始
onCreate()
函数。 - 您的应用初始化的任何全局单例对象。
- 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环。
问题的解决方案
无论问题在于不必要的初始化还是磁盘 I/O,解决方案都是延迟初始化。换句话说,只初始化立即需要的对象。不要创建全局静态对象,而是转向单例模式,应用只在第一次需要时初始化对象。
此外,请考虑使用像Hilt这样的依赖项注入框架,该框架会在第一次注入对象和依赖项时创建它们。
如果您的应用使用内容提供程序在启动时初始化应用组件,请考虑改用应用启动库。
繁重的活动初始化
活动创建通常需要大量高开销的工作。通常,有很多机会优化这项工作以实现性能改进。常见的此类问题包括:
- 膨胀大型或复杂的布局。
- 阻止磁盘上的屏幕绘制或网络 I/O。
- 加载和解码位图。
- 光栅化
VectorDrawable
对象。 - 初始化活动的其它子系统。
诊断问题
在这种情况下,方法跟踪和内联跟踪也可能很有用。
方法跟踪
使用 CPU Profiler 时,请注意您应用的Application
子类构造函数和 com.example.customApplication.onCreate()
方法。
如果工具显示这些方法需要很长时间才能完成执行,请进一步探究以查看在那里正在执行哪些工作。
内联跟踪
使用内联跟踪来调查可能的罪魁祸首,包括以下内容:
- 您应用的初始
onCreate()
函数。 - 它初始化的任何全局单例对象。
- 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环。
问题的解决方案
有很多潜在的瓶颈,但两个常见的问题和解决方法如下:
- 您的视图层次结构越大,应用膨胀它所需的时间就越长。您可以采取以下两个步骤来解决此问题:
- 通过减少冗余或嵌套布局来简化您的视图层次结构。
- 不要膨胀启动时不需要可见的 UI 部分。相反,使用
ViewStub
对象作为应用可以在更合适的时间膨胀的子层次结构的占位符。
- 在主线程上进行所有资源初始化也会减慢启动速度。您可以按如下方式解决此问题:
- 移动所有资源初始化,以便应用可以在不同的线程上延迟执行它。
- 让应用加载和显示您的视图,然后稍后更新依赖于位图和其他资源的可视属性。
自定义启动画面
如果您以前使用以下方法之一在 Android 11(API 级别 30)或更低版本中实现自定义启动画面,您可能会看到启动期间添加了额外的时间:
- 使用
windowDisablePreview
主题属性关闭系统在启动期间绘制的初始空白屏幕。 - 使用专用的
Activity
。
从 Android 12 开始,需要迁移到SplashScreen
API。此 API 可实现更快的启动时间,并允许您以以下方式调整启动画面:
- 设置主题以更改启动画面的外观。
- 使用
windowSplashScreenAnimationDuration
控制启动画面显示的时间长度。 - 自定义启动画面动画,并优雅地处理关闭启动画面的动画。
此外,兼容性库反向移植了SplashScreen
API,以实现向后兼容性,并为所有 Android 版本创建一致的启动画面显示外观。
请参阅启动画面迁移指南了解详细信息。
推荐内容
- 注意:JavaScript 关闭时显示链接文本
- 渲染缓慢
- 捕获宏基准测试指标
- 创建基线配置文件{:#creating-profile-rules}