用户希望应用能够快速加载并响应迅速。启动时间过长的应用无法满足此预期,可能会让用户感到失望。这种糟糕的体验可能会导致用户在 Play 商店中给您的应用差评,甚至彻底放弃您的应用。
本页面提供相关信息,可帮助您优化应用的启动时间,包括启动流程的内部机制概览、如何分析启动性能以及一些常见的启动时间问题及其解决方法。
了解不同的应用启动状态
应用启动可以分为三种状态:冷启动、温启动或热启动。每种状态都会影响应用对用户可见所需的时间。在冷启动中,您的应用从头开始启动。在其他状态下,系统需要将正在运行的应用从后台移到前台。
建议您始终基于冷启动的假设进行优化。这样做也可以改善温启动和热启动的性能。
为了优化应用的快速启动,了解系统和应用层面发生的情况以及它们在每种状态下如何交互非常有用。
确定应用启动的两个重要指标是首次显示时间 (TTID) 和完全绘制时间 (TTFD)。TTID 是显示第一帧所需的时间,而 TTFD 是应用变得完全可交互所需的时间。两者都同样重要,因为 TTID 可让用户知道应用正在加载,而 TTFD 则是应用实际可用时的时间。如果其中任何一个时间过长,用户可能会在应用完全加载之前退出您的应用。
冷启动
冷启动是指应用从头开始启动。这意味着在此启动之前,系统进程会创建应用的进程。冷启动发生在以下情况:例如,您的应用是设备启动后或系统终止应用后首次启动。
这种类型的启动对最大程度缩短启动时间带来了最大的挑战,因为系统和应用需要执行的工作量比其他启动状态下更多。
冷启动开始时,系统有以下三项任务:
- 加载并启动应用。
- 在应用启动后立即为应用显示一个空白启动窗口。
- 创建应用进程。
系统创建应用进程后,应用进程负责以下阶段:
- 创建应用对象。
- 启动主线程。
- 创建主 Activity。
- 膨胀视图。
- 布局屏幕。
- 执行初始绘制。
当应用进程完成首次绘制时,系统进程会换出显示的背景窗口,将其替换为主 Activity。此时,用户即可开始使用该应用。
图 1 展示了系统和应用进程之间如何交接工作。

在应用创建和 Activity 创建过程中可能会出现性能问题。
应用创建
您的应用启动时,空白启动窗口会一直停留在屏幕上,直到系统首次完成应用绘制。此时,系统进程会为您的应用替换启动窗口,从而让用户可以与应用互动。
如果您在自己的应用中替换 Application.onCreate()
,系统会调用应用对象上的 onCreate()
方法。随后,应用会生成主线程(也称为 UI 线程),并指派它创建您的主 Activity。
从此时起,系统和应用层面的进程会按照应用生命周期阶段执行。
Activity 创建
应用进程创建 Activity 后,Activity 会执行以下操作:
- 初始化值。
- 调用构造函数。
- 调用适合 Activity 当前生命周期状态的回调方法,例如
Activity.onCreate()
。
通常,onCreate()
方法对加载时间的影响最大,因为它执行开销最高的工作:加载和膨胀视图,以及初始化 Activity 运行所需的相应对象。
温启动
温启动包含冷启动期间发生的某些操作的子集。同时,它比热启动的开销更大。有很多可能被视为温启动的状态,例如以下状态:
用户退出应用,但随后又重新启动它。该进程可能会继续运行,但应用必须通过调用
onCreate()
从头开始重新创建 Activity。系统将您的应用从内存中逐出,然后用户重新启动它。该进程和 Activity 需要重新启动,但任务可以在一定程度上受益于传入
onCreate()
的已保存实例状态 Bundle。
热启动
应用的热启动开销低于冷启动。在热启动中,系统会将您的 Activity 移到前台。如果您的应用的所有 Activity 仍驻留在内存中,则应用可以避免重复进行对象初始化、布局膨胀和渲染。
但是,如果因内存整理事件(例如 onTrimMemory()
)而清除了一些内存,则需要根据热启动事件重新创建这些对象。
热启动在屏幕上显示的行为与冷启动方案相同。系统进程会显示空白屏幕,直到应用完成 Activity 渲染。

如何在 Perfetto 中识别应用启动
要调试应用启动问题,确定应用启动阶段具体包含哪些内容会很有帮助。如需在 Perfetto 中识别整个应用启动阶段,请按以下步骤操作:
在 Perfetto 中,找到带有 Android 应用启动派生指标的行。如果您没有看到,请尝试使用设备上的系统跟踪应用捕获跟踪。
图 3. Perfetto 中的 Android 应用启动派生指标切片。 点击关联的切片,然后按 m 以选择该切片。切片周围会出现方括号,表示所用的时间。当前选择标签页中也会显示持续时间。
点击图钉图标以固定 Android 应用启动行,当您将指针悬停在该行上方时,该图标会显示出来。
滚动到相关应用所在的行,然后点击第一个单元格以展开该行。
按 w 可放大主线程(通常在顶部)(按 s、a、d 分别可缩小、左移和右移)。
图 4. Android 应用启动派生指标切片位于应用主线程旁边。 派生指标切片可让您更轻松地查看应用启动中具体包含哪些内容,以便您可以继续进行更详细的调试。
使用指标检查和改进启动
要正确诊断启动时间性能,您可以跟踪显示应用启动所需时间的指标。Android 提供了多种方法来显示您的应用存在问题并帮助您诊断问题。Android vitals 可以提醒您问题正在发生,而诊断工具可以帮助您诊断问题。
利用启动指标的优势
Android 使用首次显示时间 (TTID) 和完全绘制时间 (TTFD) 指标来优化应用的冷启动和温启动。Android Runtime (ART) 使用这些指标的数据来高效地预编译代码,以优化未来的启动。
更快的启动速度可延长用户与应用的持续互动时间,从而减少过早退出、重新启动实例或导航到其他应用的情况。
Android vitals
当您的应用启动时间过长时,Android vitals 会在 Play 管理中心向您发出提醒,从而帮助提高应用的性能。
Android vitals 认为您的应用出现以下启动时间过长的情况:
Android vitals 使用首次显示时间 (TTID) 指标。如需了解 Google Play 如何收集 Android vitals 数据,请参阅 Play 管理中心文档。
首次显示时间
首次显示时间 (TTID) 是指显示应用界面第一帧所需的时间。此指标衡量应用生成其第一帧所需的时间,包括冷启动期间的进程初始化、冷启动或温启动期间的 Activity 创建,以及显示第一帧。降低应用的 TTID 有助于改善用户体验,让用户可以快速看到您的应用启动。Android Framework 会自动为每个应用报告 TTID。在优化应用启动时,建议您实现 reportFullyDrawn
以获取直至 TTFD 的信息。
TTID 是一个时间值,表示包括以下事件序列在内的总耗时:
- 启动进程。
- 初始化对象。
- 创建并初始化 Activity。
- 膨胀布局。
- 首次绘制应用。
检索 TTID
要查找 TTID,请在 Logcat 命令行工具中搜索包含名为 Displayed
的值的输出行。此值即为 TTID,与以下示例类似,其中 TTID 为 3s534ms
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms
如需在 Android Studio 中查找 TTID,请从过滤条件下拉菜单中停用 Logcat 视图中的过滤条件,然后查找 Displayed
时间,如图 5 所示。必须停用过滤条件,因为此日志由系统服务器而非应用本身提供。

Displayed
值。Logcat 输出中的 Displayed
指标不一定能捕获所有资源加载并显示所需的时间。它会忽略布局文件中未引用的资源,或应用作为对象初始化的一部分创建的资源。它排除这些资源的原因是加载它们是一个内联过程,并且不会阻止应用的初始显示。
有时,Logcat 输出中的 Displayed
行包含一个额外字段,用于显示总时间。例如:
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)
在这种情况下,首次时间测量仅针对首次绘制的 Activity。total
时间测量从应用进程启动时开始,可以包括先启动但未在屏幕上显示任何内容的另一个 Activity。只有当单个 Activity 和总启动时间之间存在差异时,才会显示 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) 是指应用变为可供用户互动所需的时间。它报告的是显示应用界面的第一帧以及初始帧显示后异步加载的内容所需的时间。通常,这是应用报告的从网络或磁盘加载的主要内容。换句话说,TTFD 既包含 TTID,也包含应用可用所需的时间。降低应用的 TTFD 有助于改善用户体验,让用户可以快速与您的应用互动。
系统会在 Choreographer
调用 Activity 的 onDraw()
方法时以及首次调用此方法时确定 TTID。但是,系统不知道何时确定 TTFD,因为每个应用的行为都不同。要确定 TTFD,应用需要向系统发出信号,告知其何时达到完全绘制状态。
检索 TTFD
要查找 TTFD,请通过调用 ComponentActivity
的 reportFullyDrawn()
方法来发出完全绘制状态的信号。reportFullyDrawn
方法会报告应用何时完全绘制并处于可用状态。TTFD 是从系统收到应用启动 intent 到调用 reportFullyDrawn()
之间经过的时间。如果您不调用 reportFullyDrawn()
,则不会报告 TTFD 值。
要测量 TTFD,请在您完全绘制界面和所有数据后调用 reportFullyDrawn()
。请勿在系统测量到第一个 Activity 的窗口首次绘制并显示之前调用 reportFullyDrawn()
,因为这样系统会报告系统测量的时间。换句话说,如果您在系统检测到 TTID 之前调用 reportFullyDrawn()
,则系统会将 TTID 和 TTFD 都报告为相同的值,并且该值就是 TTID 值。
当您使用 reportFullyDrawn()
时,Logcat 会显示如下例所示的输出,其中 TTFD 为 1s54ms
system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms
Logcat 输出有时包含 total
时间,如首次显示时间中所述。
如果您的显示时间比您想要的慢,您可以尝试找出启动过程中的瓶颈。
在您知道已达到完全绘制状态的基本情况下,可以使用 reportFullyDrawn()
来发出完全绘制状态的信号。但是,在后台线程必须在达到完全绘制状态之前完成后台工作的情况下,您需要延迟调用 reportFullyDrawn()
以获得更准确的 TTFD 测量值。如需了解如何延迟调用 reportFullyDrawn()
,请参阅以下部分。
提高启动计时准确性
如果您的应用正在执行延迟加载,并且初始显示不包含所有资源(例如您的应用正在从网络获取图片时),您可能需要延迟调用 reportFullyDrawn
,直到您的应用变得可用,这样您就可以将列表填充作为基准计时的一部分。
例如,如果界面包含动态列表(例如 RecyclerView
或延迟列表),则该列表可能会由后台任务填充,该任务在列表首次绘制后(因此在界面标记为完全绘制后)完成。在这种情况下,列表填充不包含在基准测试中。
要将列表填充作为基准计时的一部分,请使用 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 {
fullyDrawnReporter.addReporter()
lifecycleScope.launch(Dispatchers.IO) {
// Perform the background operation.
fullyDrawnReporter.removeReporter()
}
fullyDrawnReporter.addReporter()
lifecycleScope.launch(Dispatchers.IO) {
// 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 性能剖析器。如需了解详情,请参阅使用 CPU 性能剖析器检查 CPU 活动。
您还可以通过在应用和 Activity 的 onCreate()
方法内部进行内联跟踪来深入了解潜在的瓶颈。如需了解内联跟踪,请参阅 Trace
函数文档和系统跟踪概览。
解决常见问题
本部分讨论了几个通常会影响应用启动性能的问题。这些问题主要涉及初始化应用和 Activity 对象以及加载屏幕。
应用初始化开销大
如果您的代码替换 Application
对象并在初始化该对象时执行大量工作或复杂逻辑,则启动性能可能会受到影响。如果您的 Application
子类执行尚不需要执行的初始化,则您的应用可能会在启动期间浪费时间。
某些初始化可能完全不必要,例如当应用实际因 intent 启动时,初始化主 Activity 的状态信息。对于 intent,应用仅使用先前初始化状态数据的子集。
应用初始化期间的其他挑战包括影响大或数量多的垃圾回收事件,或者与初始化同时发生的磁盘 I/O,这会进一步阻止初始化过程。垃圾回收在 Dalvik 运行时中尤其需要考虑;Android Runtime (ART) 会并发执行垃圾回收,从而最大程度地降低该操作的影响。
诊断问题
您可以使用方法跟踪或内联跟踪来尝试诊断问题。
方法跟踪
运行 CPU 性能剖析器后发现,callApplicationOnCreate()
方法最终会调用您的 com.example.customApplication.onCreate
方法。如果工具显示这些方法需要很长时间才能完成执行,请进一步探索以查看其中正在发生的工作。
内联跟踪
使用内联跟踪来调查可能的罪魁祸首,包括以下各项:
- 您应用的初始
onCreate()
函数。 - 您的应用初始化的任何全局单例对象。
- 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环。
问题解决方案
无论问题在于不必要的初始化还是磁盘 I/O,解决方案都是延迟初始化。换句话说,仅初始化立即需要的对象。不要创建全局静态对象,而是采用单例模式,让应用仅在首次需要时才初始化对象。
此外,考虑使用依赖项注入框架(例如 Hilt),该框架在首次注入对象和依赖项时创建它们。
如果您的应用使用内容提供程序在启动时初始化应用组件,请考虑改用App Startup 库。
Activity 初始化开销大
Activity 创建通常会带来许多高开销的工作。通常,有机会优化这项工作以实现性能提升。此类常见问题包括:
- 膨胀大型或复杂布局。
- 在磁盘上或网络 I/O 上阻塞屏幕绘制。
- 加载和解码位图。
- 栅格化
VectorDrawable
对象。 - 初始化 Activity 的其他子系统。
诊断问题
在这种情况下,方法跟踪和内联跟踪也都很有用。
方法跟踪
使用 CPU 性能剖析器时,请注意您应用的 Application
子类构造函数和 com.example.customApplication.onCreate()
方法。
如果工具显示这些方法需要很长时间才能完成执行,请进一步探索以查看其中正在发生的工作。
内联跟踪
使用内联跟踪来调查可能的罪魁祸首,包括以下各项:
- 您应用的初始
onCreate()
函数。 - 它初始化的任何全局单例对象。
- 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环。
问题解决方案
存在许多潜在瓶颈,但以下是两个常见问题和补救措施:
- 视图层次结构越大,应用膨胀所需的时间就越长。解决此问题的两个步骤如下:
- 通过减少冗余或嵌套布局来平整视图层次结构。
- 不要膨胀启动期间不需要可见的界面部分。而是使用
ViewStub
对象作为子层次结构的占位符,应用可以在更合适的时间膨胀它。
- 将所有资源初始化都放在主线程上也会减慢启动速度。您可以按如下方式解决此问题:
- 移动所有资源初始化,以便应用可以在不同线程上延迟执行它。
- 让应用加载并显示您的视图,然后稍后更新依赖于位图和其他资源的视觉属性。
自定义启动画面
如果您之前使用以下方法之一在 Android 11(API 级别 30)或更早版本中实现自定义启动画面,则可能会在启动期间看到额外的时间开销:
- 使用
windowDisablePreview
主题属性关闭系统在启动时绘制的初始空白屏幕。 - 使用专用的
Activity
。
从 Android 12 开始,必须迁移到 SplashScreen
API。此 API 可缩短启动时间,并允许您通过以下方式调整启动画面:
- 设置主题以更改启动画面的外观。
- 使用
windowSplashScreenAnimationDuration
控制启动画面的显示时长。 - 自定义启动画面动画,并优雅地处理关闭启动画面的动画。
此外,兼容性库还反向移植了 SplashScreen
API,以实现向后兼容性,并为所有 Android 版本的启动画面显示创建一致的外观和风格。
如需了解详情,请参阅启动画面迁移指南。
为您推荐
- 注意:禁用 JavaScript 时会显示链接文本
- 渲染缓慢
- 捕获 Macrobenchmark 指标
- 创建基准配置文件{:#creating-profile-rules}