应用启动时间

用户希望应用能够快速加载并及时响应。启动时间过长的应用无法满足这个期望,并且可能会令用户失望。这种糟糕的体验可能会导致用户在 Play 商店针对您的应用给出很低的评分,甚至完全抛弃您的应用。

本页面提供了有助于优化应用启动时间的信息,包括启动流程的内部机制概述、如何分析启动性能,以及一些常见的启动时间问题和有关如何解决这些问题的提示。

了解不同的应用启动状态

应用有三种启动状态:冷启动、温启动或热启动。每种状态都会影响应用向用户显示所需的时间。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。

我们建议您始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。

如需优化应用以实现快速启动,了解系统和应用层面的情况以及它们在各个状态中的互动方式很有帮助。

确定应用启动时间的两个重要指标是初步显示所用时间 (TTID)完全显示所用时间 (TTFD)。TTID 是显示第一帧所用的时间,TTFD 则是应用达到可全面互动的状态所用的时间。两者同样重要,因为 TTID 让用户知道应用正在加载,TTFD 表示用户等待多长时间才能实际使用应用。如果其中任一时间过长,用户都可能会在应用完全加载之前退出应用。

冷启动

冷启动是指应用从头开始启动。这意味着,系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。

这种启动给最大限度地减少启动时间带来了最大的挑战,因为系统和应用要做的工作比在其他启动状态下更多。

在冷启动开始时,系统有以下三项任务:

  1. 加载并启动应用。
  2. 在启动后立即显示应用的空白启动窗口。
  3. 创建应用进程

系统一创建应用进程,应用进程就负责后续阶段:

  1. 创建应用对象。
  2. 启动主线程。
  3. 创建主 activity。
  4. 初始化界面。
  5. 在屏幕上绘制界面。

当应用进程完成第一次绘制时,系统进程就会换掉显示的后台窗口,将其替换为主 activity。此时,用户可以开始使用应用。

如需详细了解 Compose 中的布局阶段,请参阅 Jetpack Compose 阶段

在创建应用和创建宿主 activity 的过程中可能会出现性能问题。

应用创建

当应用启动时,空白启动窗口将保留在屏幕上,直到系统首次完成应用绘制。此时,系统进程会切换应用的启动窗口,让用户与应用互动。

如果您在自己的应用中替换了 Application.onCreate,系统将对应用对象调用 onCreate 方法。之后,应用生成主线程(也称为界面线程),并用其执行创建应用宿主 activity 的任务。

从此时开始,系统级和应用级进程根据应用生命周期阶段继续运行。

activity 创建

在应用进程创建 activity 后,activity 将执行以下操作:

  1. 初始化值。
  2. 调用构造函数。
  3. 根据 activity 的当前生命周期状态,相应地调用回调方法,如 Activity.onCreate

通常,onCreate 方法对加载时间的影响最大。在 Jetpack Compose 应用中,onCreate 方法的开销通常来自初始组合。当您的应用调用 setContent 并调用顶级可组合项时,就会发生这种情况。深层或复杂的界面层次结构,或者在主线程上执行大量计算的可组合项,都会增加此组合时间。

温启动

温启动包含了在冷启动期间发生的部分操作。同时,它的开销要比热启动高。有许多潜在状态可视为温启动,例如以下状态:

  • 用户在退出应用后又重新启动应用。进程可能继续运行,但应用必须通过调用 onCreate 从头开始重新创建 activity。

  • 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 activity 需要重启,但传递到 onCreate 的已保存的实例 state bundle 对于完成此任务有一定助益。

热启动

应用的热启动开销比冷启动更低。在热启动中,系统会将应用的宿主 activity 带到前台。如果应用的所有界面仍驻留在内存中,则应用可以避免重复执行对象初始化、界面初始化和呈现。

但是,如果一些内存为响应内存整理事件(如 onTrimMemory)而被完全清除,则需要为了响应热启动事件而重新创建相应的对象。

热启动显示的屏幕上行为和冷启动场景相同。在应用完成 activity 呈现之前,系统进程将显示空白屏幕。

图 1. 包含各种启动状态及其各自进程的示意图,其中每个状态都将从绘制的第一帧开始。

如何在 Perfetto 中识别应用启动

如需调试应用启动问题,确定应用启动阶段包含哪些确切内容会很有帮助。如需在 Perfetto 中识别整个应用启动阶段,请按以下步骤操作:

  1. 在 Perfetto 中,找到包含“Android App Startups”派生指标的行。如果您没有看到该行,请尝试使用设备上的系统跟踪应用捕获跟踪记录。

    图 2. Perfetto 中的“Android App Startups”派生指标 slice。
  2. 点击关联的 slice,然后按 m 选择该 Slice。该 Slice 会被括出显示,并标注所用时间。时长也会显示在 Current selection 标签页中。

  3. 点击图钉图标以固定“Android App Startups”行(将鼠标悬停在该行上即可看到图钉图标)。

  4. 滚动到相关应用所在的行,然后点击第一个单元格以展开该行。

  5. w 放大主线程(通常位于顶部),按 s、a、d 分别可缩小线程、向左移动、向右移动。

    图 3. 位于应用主线程旁的“Android App Startups”派生指标 slice。
  6. 派生指标 slice 可让您更轻松地查看应用启动阶段包含的具体内容,以便您继续进行更详细的调试。

分析 Jetpack Compose 应用时,您可以使用组合轨迹分析来查看界面性能的精细详情。请特别注意主线程中以 Choreographer#doFrame 开头的轨迹部分。查找 Compose:recomposeCompose:layoutCompose:draw 等切片。这些指标显示了应用在以下方面花费的时间:合成、测量和放置,以及绘制特定组件。

使用指标检查和改进启动

如需正确诊断启动时间性能,您可以跟踪一些显示应用启动所需时间的指标。Android 提供了一些方式,以便在您的应用有问题时让您知道,并帮助您进行诊断。Android Vitals 可以提醒您正在发生问题,而诊断工具可以帮助您诊断问题。

使用启动指标的优势

Android 使用初步显示所用时间 (TTID)完全显示所用时间 (TTFD) 指标来优化冷应用启动和温应用启动。Android 运行时 (ART) 使用这些指标的数据来高效地预编译代码,以优化未来启动。

更快的启动速度可以促进用户与应用的持续互动,从而减少过早退出、重启实例或前往其他应用的情况。

Android Vitals

当您的应用启动时间过长时,Android Vitals 可以通过 Play 管理中心提醒您,从而帮助提升应用性能。

Android Vitals 认为您的应用存在以下启动时间过长的情况:

  • 启动用了 5 秒或更长时间。
  • 启动用了 2 秒或更长时间。
  • 启动用了 1.5 秒或更长时间。

Android Vitals 使用初步显示所用时间 (TTID) 指标。如需了解 Google Play 如何收集 Android Vitals 数据,请参阅 Play 管理中心文档

初步显示所用时间

初始显示所用时间 (TTID) 是指显示应用界面的第一帧所需的时间。此指标用于测量应用生成第一帧所用的时间,包括冷启动期间的进程初始化、冷启动或温启动期间的 activity 创建,以及显示第一帧。保持较低的应用 TTID 有助于提升用户体验,让用户看到应用快速启动。Android 框架会自动针对每个应用报告 TTID。在优化应用启动时,我们建议实现 reportFullyDrawn 以获取 TTFD 信息。

TTID 是一项时间值,表示总耗时,包括以下事件序列:

  • 启动流程。
  • 正在初始化对象。
  • 创建并初始化宿主 activity。
  • 初始化界面。
  • 首次绘制应用。

检索 TTID

如需查找 TTID,请在 Logcat 命令行工具中搜索包含名为 Displayed 的值的输出行。此值是 TTID,类似于以下示例(其中 TTID 为 3s534ms):

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

如需在 Android Studio 中查找 TTID,请从下拉过滤条件菜单中停用 Logcat 视图中的过滤条件,然后找到 Displayed 时间,如图 4 所示。 停用过滤器是必要的,因为提供此日志的是系统服务器,不是应用本身。

图 4. 停用过滤器,并在 Logcat 中查找 Displayed 值。

在所有资源完全加载并显示之前,Logcat 输出中的 Displayed 指标不一定会捕获时间。它会省去初始合成中未引用的资源(例如异步加载的数据或图片)或被应用作为对象初始化一部分创建的资源。它之所以排除这些资源,是因为加载它们是一个内嵌进程,并且不会阻止应用的初步显示。如需详细了解资源,请参阅 Compose 中的资源

有时,Logcat 输出中的 Displayed 行中会包含用于显示总时间的附加字段,如以下示例所示:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)

在这种情况下,第一个时间测量值仅针对第一个绘制的 activity(通常是宿主 activity)。total 时间测量值是从应用进程启动时开始计算,并且可以包含首次启动但未在屏幕上显示任何内容的另一个 activity。total 时间测量值仅在单个 activity 的时间和总启动时间之间存在差异时才会显示。

我们建议您在 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 有助于提升用户体验,让用户能够快速与您的应用互动。

虽然系统可以在宿主窗口渲染其初始帧时确定 TTID,但无法自动确定 TTFD。由于应用通常会异步加载其主要内容,因此系统不知道应用何时才能真正供用户完全使用。为了确定 TTFD,应用需要在达到完全绘制状态时向系统发出信号。

检索 TTFD

如需查找 TTFD,请通过调用 ComponentActivityreportFullyDrawn 方法来发出完全绘制状态信号。reportFullyDrawn 方法用于报告应用何时完全绘制完毕并处于可用状态。TTFD 是指从系统收到应用启动 intent 到调用 reportFullyDrawn 所经过的时间。如果您不调用 reportFullyDrawn,则不会报告任何 TTFD 值。

如需衡量 TTFD,请在完全绘制界面和所有数据后调用 reportFullyDrawn。请勿在第一个 activity 的窗口首次绘制并显示(由系统测量)之前调用 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,直到应用变得可用为止,这样您就可以将列表填充纳入基准时间。

例如,如果界面包含一个动态列表(如 LazyColumnLazyRow),那么可能就要在首次绘制列表之后、因此也就是在界面被标记为完全绘制之后,才通过后台任务来填充该列表。在这种情况下,基准测试不涵盖列表填充。

如需将列表填充时间也纳入基准时间,请使用 fullyDrawnReporter 获取 FullyDrawnReporter,并在应用代码中为其添加报告程序。在后台任务完成列表填充后,释放报告程序。

在所有添加的报告程序都被释放之后,FullyDrawnReporter 才会调用 reportFullyDrawn 方法。通过添加报告程序并在后台进程完成后将其释放,您可以确保启动时间数据包含列表填充时间,而不会改变面向用户的应用行为。无论所有其他任务的执行顺序如何,都应始终将 reportFullyDrawn 放在最后一步执行。

如果您的应用使用 Jetpack Compose,您可以使用以下 API 指示完全绘制状态:

  • ReportDrawn:表示可组合项已准备好立即进行互动。
  • ReportDrawnWhen:接受一个谓词(例如 list.count > 0),用于指明可组合项何时准备好进行互动。
  • ReportDrawnAfter:接受一个暂停方法,该方法一旦完成即表示可组合项已准备好进行互动。

以下示例展示了如何同时运行多个后台任务,并让每个任务都注册自己的报告程序:

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()
                }
            }
        }
    }
}
识别瓶颈

如需找出瓶颈问题,您可以使用 Android Studio CPU 性能分析器。如需了解详情,请参阅使用 CPU 性能分析器检查 CPU 活动

您也可以通过应用和 activity 的 onCreate 方法内部的内嵌跟踪记录深入了解潜在瓶颈。如需了解内嵌跟踪记录,请参阅 Trace 函数文档和系统跟踪记录概览

解决常见问题

本节讨论几个通常会影响应用启动性能的问题。这些问题主要涉及初始化应用和 activity 对象,以及屏幕加载。

密集型应用初始化

在您的代码替换 Application 对象,并在初始化该对象过程中执行密集工作或复杂逻辑时,启动性能可能会受影响。如果您的 Application 子类执行尚不需要完成的初始化,您的应用可能会在启动过程中浪费时间。

有些初始化可能完全没有必要:例如,当应用为了响应 intent 而实际上已经启动时,初始化主 activity 的状态信息就是不必要的。通过 intent,应用仅使用之前初始化状态数据的一个子集。

应用初始化过程中的其他挑战包括影响范围较大或数量众多的垃圾回收事件,或与初始化同时发生、会进一步阻止初始化过程的磁盘 I/O。垃圾回收是 Dalvik 运行时特别需要考虑的问题;Android 运行时 (ART) 同时执行垃圾回收,从而最大限度地减少该操作的影响。

诊断问题

您可以使用方法跟踪记录或内嵌跟踪记录来尝试诊断问题。

方法跟踪

运行 CPU 性能分析器显示,callApplicationOnCreate 方法最终调用您的 com.example.customApplication.onCreate 方法。如果该工具显示这些方法需要很长时间才能完成执行,请进一步探索以查看正在进行哪些工作。

内嵌跟踪记录

使用内嵌跟踪记录调查可能的问题根源,包括:

  • 应用的初始 onCreate 函数。
  • 应用初始化的任何全局单例对象。
  • 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环。

问题解决方案

不管问题在于不必要的初始化还是磁盘 I/O,解决方案都是延迟初始化。换言之,应当仅初始化立即需要的对象。采用单例模式,让应用仅在首次需要对象时初始化对象,而不是创建全局静态对象。

此外,考虑使用依赖项注入框架(如 Hilt),它们会在首次注入时创建对象和依赖项。

如果您的应用使用 content provider 在启动时初始化应用组件,请考虑改用 App Startup 库

密集型 activity 初始化

创建 activity 通常需要进行大量的高开销工作。通常有机会优化这项工作以实现性能改进。此类常见问题包括:

  • 初始化大型或复杂的界面。
  • 可组合项中的密集型初始化。
  • 阻止磁盘上的屏幕绘制或网络 I/O。
  • 加载和解码位图。
  • 栅格化 VectorDrawable 对象。
  • 初始化应用宿主 activity 的其他子系统。

诊断问题

在这种情况下,方法跟踪记录和内嵌跟踪记录同样很有用。

方法跟踪

使用 CPU 性能分析器时,请注意应用的 Application 子类构造函数和 com.example.customApplication.onCreate 方法。

如果该工具显示这些方法需要很长时间才能完成执行,请进一步探索以查看正在进行哪些工作。

内嵌跟踪记录

使用内嵌跟踪记录调查可能的问题根源,包括:

  • 应用的初始 onCreate 函数。
  • 应用初始化的任何全局单例对象。
  • 在瓶颈期间可能发生的任何磁盘 I/O、反序列化或紧密循环。

问题解决方案

潜在瓶颈有很多,但两种常见问题和补救措施如下所示:

  • 界面层次结构越大,应用初始化所需的时间就越长。解决此问题时,请考虑执行以下步骤:
    • 通过减少冗余或嵌套的可组合项,展平界面层次结构。
    • 避免在启动期间进行不必要的组合和重组。
    • 推迟合成非关键界面。
  • 在主线程上进行所有资源初始化也会降低启动速度。您可以按以下方式解决此问题:
    • 转移所有资源初始化,以便应用可以在其他线程上延迟执行。
    • 先使用占位数据加载并显示界面,稍后再更新依赖于位图和其他资源的可视属性。

如需详细了解重组,请参阅重组获取重组次数

自定义启动画面

如果您之前曾在 Android 11(API 级别 30)或更低版本中使用以下某种方法来实现自定义启动画面,则可能会增加额外的启动时间:

  • 使用 windowDisablePreview 主题属性关闭系统在启动过程中绘制的初始空白屏幕。
  • 使用专用 Activity

从 Android 12 开始,必须迁移到 SplashScreen API。此 API 可以缩短启动时间,并允许您通过以下方式调整启动画面:

此外,compat 库会向后移植 SplashScreen API 以支持向后兼容性,并在所有 Android 版本上实现一致的启动画面显示效果。

如需了解详情,请参阅启动画面迁移指南