(Chinglish翻译系列) App启动时间

用户希望App能快速响应并且快速打开。如果一个App启动速度慢,那么有可能会导致用户失望。这一系列的情况会导致用户给你5星差评并且卸载你的软件。

这篇文章提供了一些有用的信息去帮助你控制好你的App启动时间。先解释一下App启动过程,然后讨论如何去分析启动性能,最后讨论一下常见的启动问题,并且给出一些定位这些问题的小技巧。

理解App启动过程

App启动可分为三种类型,每一种类型都会影响到你App展现给用户的时间:冷启动、暖启动和热启动。在冷启动中,你的App是重零开始。在剩下的两种类型中。系统需要把在后台运行的App移到前台。我们强烈建议你总是基于冷启动进行优化,这样也可以提高暖启动和热启动的性能。

了解在App启动过程中系统发生了什么,以及他们在这过程中是如何互动的。对优化你的App启动速度是很有帮助的。

冷启动

冷启动是指一个App是重新创建的,系统进程里面没有App的进程,直到它启动了并且创建了。冷启动会发生在你的App第一次打开(从开机起第一次打开,或者系统杀掉你的App后第一次打开)。这种启动过程在最小化启动优化里面具有最大的挑战,因为这种启动比其他类型,系统和App都要做多更多的工作。

在冷启动的开始,系统有三个任务需要完成,这些任务是:

  1. 加载和启动App
  2. 在App启动后里面显示出一个空白的窗口
  3. 创建App进程

一旦系统创建完App进程,App进程就会进入下一个流程:

  1. 创建App
  2. 启动主线程
  3. 创建main activity
  4. 加载views
  5. 布局测量
  6. 初始化绘图

一旦App进程完成第一个绘制,系统会把App替换成当前活动的activity,这个时候,用户就能开始使用App了。

图一表示系统和App进程在切换过程中的工作原理

在创建App和创建Activity过程中会出现性能问题

Application creation

当你的app启动的时候,系统会加载一个空白的窗口直到系统完成app的第一次绘制,这个时候系统进程会把你的app替换当前的活动窗口,然后用户就可以开始与应用程序进行交互。

如果你有在你的app里面重写Application.OnCreate(),系统会在启动后调用OnCreate()这个方法,然后app就会创建主线程,同时也是UI线程,并且通过它来创建你的main activity。

从这一点开始,系统和应用程序级别的App将根据应用程序生命周期阶段进行。

Activity creation

在app创建了你的activity之后,activity会执行以下几个操作:

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

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

热启动

热启动比冷启动更加简单,开销更低。在热启动中,系统所需要做的就是把你的activity切换到前台。如果你app里面的所有activitys都还在内存里面,那么app可以避免重复初始化对面,布局加载和惠子。

然而,在某些时候如果内存紧张的时候,那么这些对象在热启动中就必须得重新创建。

热启动展现activity的行为跟冷启动样:先显示一个空白的窗口知道app完成第一次绘制

暖启动

暖启动包括冷启动期间发生的一些操作子集;同时,它比热启动更少的开销。有许多潜在的状态可以被视为暖启动。例如:

  • 用户点击home,app进入后台,然后重启app,app进程可能还在继续运行。但是activity被回收,app必须从头开始创建。也会调用activity的OnCreate方法。
  • 系统回收了你的app,然后用户重启app。这个时候app进程和Activity都需要重新启动,但是它们可以从onCreate()方法中的bundle恢复数据。

诊断问题

Android提供了几种方式让你知道你的app有问题,和帮助你诊断它。
Android vitals可以提醒你问题正在发生,诊断工具可以帮助你诊断问题。

Android vitals

Android vitals可以通过Goole Play的控制台在您的应用启动时间过长时提醒您,以帮助提高应用的性能。当应用程序出现一下情况时,Android vitals认为您应用的启动时间过长:

  • 冷启动超过5秒
  • 暖启动超过2秒
  • 热启动产国1.5秒

诊断启动慢问题

为了定位启动时间,你可以通过定位你的application启动花费时间。

初始化显示时间

在Android4.4(API 19)或者更高,logcat的里面有一行输出包含了关键字Displayed。这个值表示了activity从启动到显示的耗时。这个耗时包含了一下几个事件:

  1. 启动进程
  2. 初始化对象
  3. 创建和初始化Activity
  4. 绘制布局
  5. 第一次绘制app

日志输出如下:

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

为了能在Android studio里面找到耗时,你需要修改filter在你的logcat,改成No Filters,而不是你的App进程。

一旦你修改好了,你就很容易在logcat里面查找耗时,如果图二显示如何修改filters,在倒数第二行显示了耗时

Displayed有时候并不是一个完整的加载时间,它可能会遗漏布局文件中没有引用到的资源文件。例如:

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

在这种情况,一个时间仅代表第一次绘制时间,total的时间表示app进程启动包括一些activity第一次启动但没有被显示出来的时间。

你也可以通过ADB命令去计算你app启动的时间,例如:

1
2
3
4
adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN

命令行会输出一下内容:

1
2
3
4
5
6
Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete

-c-a参数表示Intent里面的<category><action>

完成的显示时间

你可以使用reportFullyDrawn())去计算一个app启动到完全显示的时间。这个在应用程序执行延迟加载的情况下会很有用。因为在延迟加载中,应用程序不会阻止窗口的初始绘制,而是异步加载资源并更新视图层次结构。

由于延迟加载,一个app的初始化显示不包含所有的资源,你可能要把资源文件和views完成加载和显示作为一个时间度量。例如,你的UI里面的text可能完成加载,但是你的图片还没从网络下载下来。

在这种情况下,你可以手动调用reportFullyDrawn())方法让系统知道你的activity已经完成了延迟加载。当你使用这个方法,logcat会输出从app创建到reportFullyDrawn())方法调用的总耗时。下面就是输出例子:

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

其中有可能输出包含total时间,这个参考初始化显示时间。

如果你看到你的耗时比你想象中的慢,你可以继续定位启动时间的瓶颈在哪里。

定位瓶颈

使用Android studio CPU profiler是一个很好定位瓶颈的方法。更多信息情况,使用CPU profiler去检查CPU情况

你还可以通过TraceSystrace工具定位瓶颈

注意常见的问题

这一章节讨论几个经常影响到启动表现的问题。这些问题大部分都是初始化app和activity对象,也有一部分是屏幕加载。

繁重的app初始化任务

当你在代码里面重写了Application对象,并且在里面执行了大量繁重的任务和复杂逻辑的时候.如果你在app初始化里面做了些非必要初始化的工作,那么就会浪费了启动时间。有些初始化是没必要的:例如,当app通过一个intent启动,然后初始化主activity的数据,在intent里面,app只用到了之前初始化好的部分信息。这样就会造成浪费。

另一部分的原因是当app执行初始化的时候,系统在执行垃圾回收。在早期Dalvik虚拟机的时候,垃圾回收会阻塞初始化进程。在Art虚拟机的时候,垃圾回收机制改成了并发的,最小的回收策略。那么就不会阻塞初始化进程。

诊断问题

你可以使用方法跟踪或内联跟踪来尝试诊断问题。

方法跟踪

使用CPU Profiler 来看callApplicationOnCreate())方法,最后会去到com.example.customApplication.onCreate方法,如果工具显示这些方法花了很上时间去执行,表示你需要查看一下到底在里面做了什么导致耗时。

行内跟踪

使用行内跟踪来定位一下情况:

  • 你的app初始化方法onCreate
  • 任何在你的app初始化的时候存在的全局单例对象
  • 任何硬盘I/O,反序列化,或者循环可能会导致耗时

解决方案

有很多情况会导致耗时,但是两个常见的问题和解决方案如下:

  • view的层级越深,那么app就需要用更多的时间去渲染它。有两个办法可以解决这个问题:
    • 通过减少冗余或者使用嵌套布局来减少view的层级
    • 在启动过程中,不去渲染看不见的UI.可以使用ViewStub来作为占位符,然后app可以在适当的时机把它渲染出来
  • 在主线程进行所有的资源初始化也会导致启动耗时。你可以按照以下方式来解决问题。
    • 在其他线程进行延迟加载资源。
    • Allow the app to load and display your views, and then later update visual properties that are dependent on bitmaps and other resources.(这句理解不了)

启动主题

你可能希望对应用程序设置了主题,以便应用程序的启动屏幕的主题和应用程序的其他部分保持一致,而不是与系统主题一致。这样做可以隐藏一个缓慢的活动启动。

一个常见的设置启动屏幕主题的方法是使用windowDisablePreview主题属性来关闭启动应用程序时系统进程绘制的初始空白屏幕。然而,这种方式会导致app不支持启动预览窗口从而导致更长的启动时间,这个时候用户在activity启动的过程中会怀疑是否App有启动。

诊断问题

你可以通过在用户启动应用时观察响应缓慢来诊断此问题。在这种情况下,屏幕似乎被冻结,或者点击任何东西都无响应。

解决问题

我们强烈推荐不要禁止预览窗口,而是遵循常见的Material Design开发模式。你可以使用activity的windowBackground属性来自定义简单的启动图片。

例如,你可以创建一个新的的图片文件和在布局文件的xml使用它:

布局文件xml:

1
2
3
4
5
6
7
8
9
10
<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文件:

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

最简单的变换主题的方式是通过在super.onCreate()setContentView()之前调用setTheme(R.style.AppTheme))方法:

java:

1
2
3
4
5
6
7
8
9
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);
// ...
}
}

Kotlin:

1
2
3
4
5
6
7
8
9
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);
// ...
}
}
-------------本文结束感谢您的阅读-------------
您的支持将是我最大的动力