应用启动时间

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

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

了解不同的应用启动状态

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

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

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

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

冷启动

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

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

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

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

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

  1. 创建应用对象。
  2. 启动主线程。
  3. 创建主 activity。
  4. 膨胀视图。
  5. 创建屏幕布局。
  6. 执行初步绘制。

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

图 1 显示系统进程和应用进程之间如何交接工作。

图 1. 以可视化方式呈现应用冷启动的重要部分。

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

应用创建

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

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

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

activity 创建

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

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

通常,onCreate() 方法对加载时间的影响最大,因为它执行工作的开销最高:加载和膨胀视图,以及初始化运行 activity 所需的对象。

温启动

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

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

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

热启动

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

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

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

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

如何在 Perfetto 中识别应用启动

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

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

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

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

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

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

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

使用指标检查和改进初创公司

如需正确诊断启动时间性能,您可以跟踪一些显示应用启动所需时间的指标。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 为 3s534 毫秒):

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

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

图 5. 停用了 Logcat 中的过滤器和 Displayed 值。

在所有资源加载并显示之前,Logcat 输出中的 Displayed 指标不一定会捕获时间。它会省去布局文件中未引用的资源或被应用作为对象初始化一部分创建的资源。它之所以排除这些资源,是因为加载它们是一个内嵌进程,并且不会阻止应用的初步显示。

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

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

在这种情况下,第一个时间测量值仅针对第一个绘制的 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 保持在低水平有助于用户快速与应用互动,从而改善用户体验。

系统会在 Choreographer 调用 activity 的 onDraw() 方法以及知道自己首次调用该方法时确定 TTID。但是,系统不知道何时确定 TTFD,因为每个应用的行为方式不同。为了确定 TTFD,应用需要在达到完全绘制状态时向系统发出信号。

检索 TTFD

如需查找 TTFD,请调用 ComponentActivityreportFullyDrawn() 方法来指示完全绘制状态。reportFullyDrawn 方法会报告应用何时完全绘制以及何时处于可用状态。TTFD 是从系统收到应用启动 intent 到调用 reportFullyDrawn() 所用的时间。如果您不调用 reportFullyDrawn(),则系统不会报告 TTFD 值。

如需测量 TTFD,请在完整绘制界面和所有数据后调用 reportFullyDrawn()。请勿在系统首次绘制和显示第一个 activity 的窗口之前调用 reportFullyDrawn(),因为之后系统会报告系统测量的时间。换言之,如果您在系统检测到 TTID 之前调用 reportFullyDrawn(),系统会将 TTID 和 TTFD 报告为同一值,并且这个值就是 TTID 值。

当您使用 reportFullyDrawn() 时,Logcat 会显示如下例所示的输出,其中 TTFD 为 1s54 毫秒:

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 {
                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 性能分析器。如需了解详情,请参阅使用 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、反序列化或紧密循环。

问题解决方案

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

  • 您的视图层次结构越大,应用膨胀它所花的时间就越长。您可以执行以下两个步骤来解决此问题:
    • 通过减少冗余或嵌套布局,展平您的视图层次结构。
    • 不要膨胀在启动期间无需显示的界面部分,而是使用 ViewStub 对象作为应用可以在更合适的时间膨胀的子层次结构的占位符。
  • 在主线程上进行所有资源初始化也会降低启动速度。您可以按以下方式解决此问题:
    • 转移所有资源初始化,以便应用可以在其他线程上延迟执行。
    • 允许应用加载并显示您的视图,稍后再更新依赖于位图和其他资源的可视属性。

自定义启动画面

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

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

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

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

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