应用启动时间

用户希望应用能够快速加载并响应迅速。启动时间过长的应用 无法达到这个期望,并且可能会令用户失望。这种糟糕的体验可能会导致用户在 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() 的已保存实例状态 bundle 中获取。

热启动

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

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

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

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

如何在 Perfetto 中识别应用启动

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

  1. 在 Perfetto 中,找到包含“Android App Startups”派生指标的行。如果 如果没有看到,请尝试使用设备端系统跟踪 app

    图 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 视图中的过滤器: filter 下拉菜单,然后找到 Displayed 时间,如图 5 所示。 停用过滤器是必要的,因为应用是系统服务器,而不是应用 本身,将提供此日志。

图 5. 已停用的过滤条件和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 运行应用来测量 TTID activity 管理器命令。示例如下:

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() 方法,并在知道自己首次调用该方法时发出调用。 但是,系统不知道何时确定 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 或延迟列表,可能会由 列表首次绘制后,因此在界面被标记为完全绘制之后。 在这种情况下,基准化不会考虑名单总体情况。

要将列表填充情况作为基准时间的一部分包含在内,请获取 FullyDrawnReporter(使用 getFullyDrawnReporter()),并添加 添加到应用代码中。完成后台任务后释放报告程序 完成填充列表的操作。

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、反序列化或紧密循环。

问题解决方案

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

  • 您的视图层次结构越大,应用膨胀它所花的时间就越长。二 您可以采取以下措施解决此问题: <ph type="x-smartling-placeholder">
      </ph>
    • 通过减少冗余或嵌套布局,展平您的视图层次结构。
    • 不要膨胀在启动期间不需要显示的界面部分。 请改用 ViewStub 对象作为子层次结构的占位符 让应用能够在更合适的时间膨胀
  • 在主线程上进行所有资源初始化也会降低启动速度。您可以按以下方式解决此问题:
    • 转移所有资源初始化,以便应用可以对 另一个线程。
    • 让应用加载并显示您的视图,稍后更新视觉元素 依赖于位图和其他资源的属性。

自定义启动画面

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

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

从 Android 12 开始,必须迁移到 SplashScreen API。 此 API 可缩短启动时间,并可让您通过 方法:

此外,compat 库向后移植了 SplashScreen API, 并打造一致的启动画面和外观, 所有 Android 版本的屏幕显示

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