应用启动时间

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

本文档提供的信息可以帮助您优化应用的启动时间。首先介绍启动过程的内部机制。然后,讨论如何剖析启动性能。最后,介绍一些常见的启动时间问题,并给出一些有关如何解决这些问题的提示。

了解应用启动内部机制

应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动或热启动。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。建议您始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。

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

冷启动

冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。这种启动给最大限度地减少启动时间带来了最大的挑战,因为系统和应用要做的工作比在另外两种启动状态中更多。

在冷启动开始时,系统有三个任务,它们是:

  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 所需的对象。

热启动

应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将您的 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局膨胀和呈现。

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

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

温启动

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

  • 用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。
  • 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存的实例 state bundle 对于完成此任务有一定助益。

检测和诊断问题

Android 提供多种方式,让您知道您的应用有问题,并帮助您进行诊断。Android Vitals 可以提醒您将要发生问题,而诊断工具可以帮助您诊断问题。

Android Vitals

当您的应用启动时间过长时,Android Vitals 可以通过 Play 管理中心提醒您,从而帮助提升应用性能。Android Vitals 在您的应用出现以下情况时将其启动时间视为过长:

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

Android Vitals 不报告启动数据。如需了解 Google Play 如何收集 Android Vitals 数据,请参阅 Play 管理中心文档。

诊断启动时间过长的问题

为了正确诊断启动时间性能,您可以跟踪一些显示应用启动所需时间的指标。

初步显示所用时间

在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 Displayed 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。经过的时间包括以下事件序列:

  1. 启动进程。
  2. 初始化对象。
  3. 创建并初始化 Activity。
  4. 扩充布局。
  5. 首次绘制应用。

报告的日志行类似于以下示例:

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

如果您从命令行或在终端中跟踪 logcat 输出,查找经过的时间很简单。要在 Android Studio 中查找经过的时间,必须在 logcat 视图中停用过滤器。停用过滤器是必要的,因为提供此日志的是系统服务器,不是应用本身。

一旦进行了正确的设置,即可轻松搜索正确术语来查看时间。图 2 展示了一个 logcat 输出示例,其中显示了如何停用过滤器,并且在输出内容的倒数第二行中显示了 Displayed 时间。


图 2. 在 logcat 中停用过滤器并查找 Displayed 值。

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

有时,logcat 输出中的 Displayed 行中会包含一个总时间的附加字段。例如:

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

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

您也可以使用 ADB Shell Activity Manager 命令运行应用来测量初步显示所用时间。示例如下:

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 为可选参数,可让您为 intent 指定 <category><action>

完全显示所用时间

您可以使用 reportFullyDrawn() 方法测量从应用启动到完全显示所有资源和视图层次结构所用的时间。在应用执行延迟加载时,此数据会很有用。在延迟加载中,应用不会阻止窗口的初步绘制,但会异步加载资源并更新视图层次结构。

如果由于延迟加载,应用的初步显示不包括所有资源,您可能会将完全加载和显示所有资源及视图视为单独的指标:例如,您的界面可能已完全加载,并绘制了一些文本,但尚未显示应用必须从网络中获取的图片。

要解决此问题,您可以手动调用 reportFullyDrawn(),让系统知道您的 Activity 已完成延迟加载。当您使用此方法时,logcat 显示的值为从创建应用对象到调用 reportFullyDrawn() 时所用的时间。以下是 logcat 输出的示例:

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

logcat 输出有时包含 total 时间,如初步显示所用时间中所述。

如果您发现您的显示时间比希望的时间长,则可以继续尝试识别启动过程中的瓶颈。

识别瓶颈

查找瓶颈的一个好方法是使用 Android Studio CPU 性能剖析器。相关信息请参阅使用 CPU 性能剖析器检查 CPU 活动

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

注意常见问题

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

密集型应用初始化

在您的代码替换 Application 对象,并在初始化该对象过程中执行密集工作或复杂逻辑时,启动性能可能会受影响。如果您的应用子类执行尚不需要完成的初始化,则您的应用可能会在启动过程中浪费时间。有些初始化可能完全没有必要:例如,当应用为了响应 intent 而实际上已经启动时,初始化主 Activity 的状态信息就是不必要的。通过 intent,应用仅使用之前初始化状态数据的一个子集。

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

诊断问题

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

方法跟踪记录

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

内嵌跟踪记录

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

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

问题解决方案

不管问题在于不必要的初始化还是磁盘 I/O,解决方案都会调用延迟初始化对象:仅初始化立即需要的对象。例如,不创建全局静态对象,而是转为单例模式,其中应用仅在第一次访问对象时初始化它们。此外,考虑使用依赖注入框架(如 Dagger),它们会在首次注入时创建对象和依赖项。

密集型 Activity 初始化

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

  • 扩充大型或复杂的布局。
  • 阻止磁盘上的屏幕绘制或网络 I/O。
  • 加载和解码位图。
  • 栅格化 VectorDrawable 对象。
  • 初始化 Activity 的其他子系统。

诊断问题

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

方法跟踪记录

使用 CPU Profiler 时,请注意应用的 Application 子类构造函数和 com.example.customApplication.onCreate() 方法。

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

内嵌跟踪记录

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

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

问题解决方案

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

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

带主题背景的启动屏幕

您可能希望为应用的加载体验设置主题背景,从而使应用的启动屏幕在主题背景上与应用的其余部分保持一致,而不是与系统主题背景一致。这样做可以隐藏缓慢的 Activity 启动。

实现带主题背景的启动屏幕的常见方式是使用 windowDisablePreview 主题背景属性来关闭启用应用时系统进程绘制的初始空白屏幕。但是,此方法可能导致启动时间比不抑制预览窗口的应用更长。此外,它还会使用户在 Activity 启动过程中只能等待而不会收到任何反馈,这会让用户无法确定应用是否在正常运行。

诊断问题

通常可以用于诊断此问题的方法是观察用户启动应用时,应用的响应是否很慢:在这种情况下,屏幕看起来会像是卡住了,或停止了对输入做出响应。

问题解决方案

建议遵守常见的 Material Design 模式,而不是停用预览窗口。您可以使用 Activity 的 windowBackground 主题背景属性,为启动 Activity 提供简单的自定义可绘制对象。

例如,您可以创建新的可绘制文件,并从布局 XML 和应用清单文件中引用它,如下所示:

布局 XML 文件:

    <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
      <!-- The background color, preferably the same as your normal theme -->
      <item android:drawable="@android:color/white"/>
      <!-- Your product logo - 144dp color version of your app icon -->
      <item>
        <bitmap
          android:src="@drawable/product_logo_144dp"
          android:gravity="center"/>
      </item>
    </layer-list>
    

清单文件:

    <activity ...
    android:theme="@style/AppTheme.Launcher" />
    

要切回到正常主题背景,最简单的方式是先调用 setTheme(R.style.AppTheme),然后再调用 super.onCreate()setContentView()

Kotlin

    class MyMainActivity : AppCompatActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            // Make sure this is before calling super.onCreate
            setTheme(R.style.Theme_MyApp)
            super.onCreate(savedInstanceState)
            // ...
        }
    }
    

Java

    public class MyMainActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        // Make sure this is before calling super.onCreate
        setTheme(R.style.Theme_MyApp);
        super.onCreate(savedInstanceState);
        // ...
      }
    }