优化应用启动时间

用户希望App能够快速相应和加载,应用启动缓慢会带来糟糕的用户体验,导致用户恶评,甚至会卸载你的应用。

这篇文章提供的信息能够帮助你优化应用的启动时间。首先,我们先来了解应用启动的内部原理,接下来,我们会讨论如何分析启动性能。最后,最后我们会介绍一些影响启动性能的常见问题,并会给出相应的解决办法。

应用启动原理

应用启动可以分为三种类型,冷启动,暖启动,热启动,每种类型所花费的时间是不一样的。
冷启动模式下,应用进程完全不存在,系统要新建应用进程。在另外两种模式下,系统只需要将运行中的应用从后台切换到前台。建议你针对冷启动模式做优化。冷启动速度得到提升了,暖启动和热启动也会变得很快。

那么在应用启动过程中,Android系统和应用层都做了那些操作呢?理解了它们的内部原理,将会帮助我们做好启动性能优化。

冷启动

冷启动指应用重新开始创建:在启动之前,系统进程尚未创建出应用进程。冷启动通常发生在第一次打开应用或者系统主动杀掉了你的应用的情况下。和其他启动方式相比,冷启动模式需要系统和应用做更多的初始化操作,所以优化起来也有一定的挑战。

在冷启动的开始阶段,系统需要执行三个任务:

  1. 加载和启动应用程序,
  2. 在app启动后立即显示一个空白启动窗体
  3. 创建App进程。

一旦系统创建了应用进程,应用进程就会执行下面步骤:

  1. 创建应用程序对象
  2. 启动主线程
  3. 创建Main Activity
  4. 初始化构造view
  5. 在屏幕上布局。
  6. 执行初始化绘制操作

一旦 应用进程完成了首次绘制,系统进程会用Main Activity 来替换 之前已经生成的背景窗体。这个时候,用户就可以使用app了。

图1 展示了启动过程中系统和应用程序之间的交替执行过程

android优化activity启动时间 安卓启动时间优化_android


图1 冷启动过程重要步骤展示

启动性能问题可能会出现在应用创建和主Activity 创建过程中。

Application 创建

当你的应用启动时,空白窗口将会一直存在 直到系统完成了应用的首次绘制操作,此时,系统会替换掉启动窗口,好让用户能够和App进行交互。

如果你覆写了Application.onCreate()方法,系统将会调用你的Applcation 的onCreate()方法。之后 应用程序将会创建出主线程(也叫UI 线程),并执行创建主Activity 的过程。

从这个时候开始,系统和应用程序级别的进程将按照应用程序生命周期阶段进行。

Activity 创建

当应用进程创建了Activity 后,Activity 会执行以下操作:

  1. 初始化值
  2. 调用构造方法
  3. 调用当前生命周期的回调函数,例如Activity.onCreate()

通常情况下,onCreate()方法对加载时间影响最大,因为它要执行的操作更加繁重:加载和构造view,还有初始化activity运行 所需的对象。

热启动

应用的热启动和冷启动相比,更加简单,开销也更少。在热启动过程中,系统要做的只是把你的Activity切换到前台来。如果应用的所有Activity都驻留在内存中,那么应用就可以避免重复进行对象初始化,布局加载还有渲染。

然而,如果系统执行了内存回收操作并触发了回收事件,例如 onTrimMemory() ,那么热启动时对象仍需要重建。

在屏幕窗口的操作上,热启动操作具有和冷启动的一样的过程:
系统进程将会一直显示一个空白屏幕直到应用完成主Activity的渲染。

暖启动

暖启动过程包含了冷启动过程的部分步骤,同时开销比热启动更小。
暖启动发生在这么几个场景下:

  1. 用户退出了应用,然后又重新打开。这种情况下,应用进程可以继续运行,但应用必须重建Activity 通过调用 onCreate()。
  2. 应用程序内存被系统回收, 然后用户又重新打开。这个时候,进程和Activity都需要被创建,但应用可以从Activity 的onCreate()方法的Bundle类型参数中拿到系统为我们保存的实例。

发现并定位问题

Android 提供了多种方式能让你能够发现并定位App的问题。Android vitals 可以给出问题告警,然后诊断工具可以帮你定位出问题。

Android vitals

Android vitals 可以通过Play控制台提醒您应用的启动时间过长,从而帮助您提高应用的性能。Android vitals启动时间过长的判断标准如下:

  1. 冷启动花费5秒或更长。
  2. 暖启动花费2秒或更长。
  3. 热启动花费1.5秒或更长。

daily session 指的是你App当天的启动情况。

Android vitals 并不会报告热启动的数据。关于Google Play 如何搜集Android vitals 数据的详细信息,请参考 Play Console 文档。

测量慢启动耗时

为了正确评估应用启动性能,你可以关注一些能够显示应用启动时长的数据指标。

初始化显示耗时

在Android 4.4(API level 19)和更高的版本中,logcat包含了一个名为Displayed值的输出行。这个值代表了从应用进程启动到完成Activity绘制所花费时间。这个过程包含了以事件序列:

  1. 启动应用进程,
  2. 初始化对象
  3. 创建和初始化Activity
  4. 构建布局
  5. 首次绘制应用

日志大概长这个样子:

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

如果你从命令行或者终端中追踪logcat 输出,那么很容易能够找到这个时间值。
注意你要禁用logcat过滤,因为这条日志是是系统输出的而不是App自己。设置好后你就能很容易抓取到输出的时间值。图二显示了如何禁用过滤器,并且在日志的倒数第二行可以看到Displayd字段的值。

android优化activity启动时间 安卓启动时间优化_Android_02


图2 禁用过滤器后输出了Displayed 字段

Display的值 并不一定是所有资源都加载完成后显示的总耗时,它b并不包括不被布局文件及初始化对象所引用的资源的加载时间,因为这个加载过程是一个内部过程,不阻塞应用初始内容的显示。

有时候,Displayed所在的那行输出 包含了一个附加字段total time ,例如

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

这种情况下,第一个时间值表示绘制出第一个可见Activity的耗时.后面的total time 指从应用进程的启动开始,可能会包含另一个Activity的启动,但这个Activity 并不可见。total time 只会在启动单个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 time 字段,这个我们在 初始化显示耗时部分已经讨论过。

如果你确定出你的显示耗时要比预期的长,你进一步继续尝试找出启动过程中的性能瓶颈。

确定性能瓶颈

Android Studio提供了两种方法来确定性能瓶颈,Method Tracer工具和内嵌追踪。 要了解Method Tracer,请参阅该工具的文档

如果您无法访问Method Tracer工具,或者无法在正确的时间启动该工具以获取日志信息,则可以通过在应用程序和Activity的onCreate()方法中进行内嵌跟踪来获得类似的观察结果。 要了解内嵌跟踪,请参阅Trace函数以及Systrace工具的文档。

常见问题

本节讨论经常影响应用程序启动性能的几个问题。主要是关注应用与 Activity 对象的初始化以及画面的加载。

App初始化开销大

Application 的创建过程中,如果执行复杂的逻辑或者初始化大量的对象,将会影响应用的启动体验。具体来说,就是你继承了 Application 并在初始化时执行了不必要的代码。有些初始化可能是完全不需要的,比如:保存 一个实际上由 intent 启动的 app 中的 main activity 的状态信息,通过Intent启动Activitiy,应用只会使用到已经初始化过得状态数据的一部分。

其它在 app 启动期间影响性能的操作还有垃圾回收,或者同步磁盘 I/O,这都会阻塞初始化进程。垃圾回收只是针对于 Dalvik 虚拟机来说,Art 虚拟机同步执行垃圾回收,将影响降到最低了。

诊断问题

可以使用Method tracing或inline tracing来诊断问题。

Method racing

运行Method Tracer工具,会显示出callApplicationOnCreate()方法最终调用`com.example.customApplication.onCreate方法。如果该工具显示这些方法需要很长时间才能完成执行,那么你应该好好看看这些方法到底做了哪些操作。
Inline tracing
使用内嵌追踪来找出可能的引发问题的元凶,包括:

  1. 应用的初始onCreate()函数。
  2. 应用初始化的任何全局单例对象。
  3. 任何瓶颈期间可能发生的磁盘I/O,反序列化,或紧凑的循环操作。

解决方案

不论问题是否由非必要的初始化或磁盘I/O引起,解决方案都是要延迟初始化对象:仅初始化那些立即使用的对象。例如,采用单例模式而不是创建全局静态对象,在该模式中,应用程序仅在第一次访问时才创建对象。也可以考虑使用 Dagger 之类的依赖注入框架,当第一次被注入时才创建对象和依赖。

Activity初始化操作复杂

Activity 的创建通常包含大量高负载工作。一般来说,可以通过考虑以下方面的问题来优化工作提高性能:

  1. 加载比较大或复杂的布局;
  2. 磁盘或网络 I/O 阻塞屏幕绘制;
  3. 加载和解码 bitmap;
  4. 调整 VectorDrawable 对象的大小;
  5. activity 中子系统的初始化。

问题诊断

同样,使用方法追踪和内部追踪。

方法追踪
当运行 Method Tracer 工具时,主要的关注点是 app 中 Application 子类的构造方法和 com.example.customApplication.onCreate() 方法。
如果工具显示完成执行时间较长,那么应该认真查找这部分做了哪些工作。

内部追踪
app 初次 onCreate() ;
app 初始化的任何全局单例对象;
任何在瓶颈期可能形成的磁盘I/O,反序列化或紧凑循环。

解决方案

有很多可能的瓶颈,两个常见的问题和解决方法如下:

  1. view 的层级越多,布局的时候就会花更多时间,可以通过两个步骤优化:
  1. 通过减少重复和布局嵌套是布局变得扁平化。
  2. 不要加载启动时对用户不可见的布局,可以使用 ViewStub。
  1. 把所有资源的初始化放到 main thread 中也会减慢启动速度,可以这样优化:
  1. 使用懒加载或非 main thread 初始化资源。
  2. 允许 app 先展示 view,然后再更新 bitmap 或其它可见资源。

设置启动页的主题

你可以让 app使用自定义主题加载,使 app 的启动屏幕与其它界面保持主题一致,而不是使用系统主题,这样做也可以使 activity 的启动看起来没那么慢。
通常实现启动屏幕主题化的方法是使用 windowDisablePreview 属性去关闭初次启动时的白屏。然而,使用这种方法会导致更长的启动时间,并且,当用户点击启动图标后可能会因为无界面反馈而使用户感到疑惑。

问题诊断

通常可以通过观察 app 启动时是不是没有响应来确定是不是存在问题,这种情况下,屏幕仿佛被冰冻一样卡住,对输入事件也不会有响应。

解决方案

相比于禁用 preview window,可以遵循 Material Design 模式。可以使用 acitivity 的 windowBackground 属性为启动屏幕提供一个简单的背景。
例如,可以创建一个 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>

对应的 Manifest 文件:

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

最简单的切换为正常主题的方式是在调用 super.onCreate()setContentView() 前调用 setTheme(R.style.AppTheme)

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);
    // ...
  }
}