Android应用启动时间
本文档主要帮助优化应用的启动时间。介绍启动过程的内部机制,讨论如何剖析启动性能。
应用启动内部机制
应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动或热启动。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。建议在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。
冷启动
冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。
在冷启动开始时,系统有三个任务,它们是:
- 加载并启动应用。
- 在启动后立即显示应用的空白启动窗口。
- 创建应用进程。
系统一创建应用进程,应用进程就负责后续阶段:
- 创建应用对象。
- 启动主线程。
- 创建主 Activity。
- 扩充视图。
- 布局屏幕。
- 执行初始绘制。
一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口,替换为主 Activity。此时,用户可以开始使用应用。
在创建应用和创建 Activity 的过程中可能会出现性能问题。
应用创建
当应用启动时,空白启动窗口将保留在屏幕上,直到系统首次完成应用绘制。完成后,系统进程会换掉应用的启动窗口,允许用户开始与应用互动。
如果应用中使 Application.onCreate()
过载,系统将在应用对象上调用 onCreate()
方法。之后,应用生成主线程(也称为界面线程),并用其执行创建主 Activity 的任务。
从此时开始,系统级和应用级进程根据应用生命周期阶段继续运行。
Activity 创建
在应用进程创建 Activity 后,Activity 将执行以下操作:
- 初始化值。
- 调用构造函数。
- 根据 Activity 的当前生命周期状态,相应地调用回调方法,如
Activity.onCreate()
。
通常,onCreate()
方法对加载时间的影响最大,因为它执行工作的开销最高:加载和膨胀视图,以及初始化运行 Activity 所需的对象。
热启动
应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局膨胀和呈现。
但是,如果一些内存为响应内存整理事件(如 onTrimMemory()
)而被完全清除,则需要为了响应热启动事件而重新创建相应的对象。
热启动显示的屏幕上行为和冷启动场景相同:在应用完成 Activity 呈现之前,系统进程将显示空白屏幕。
温启动
温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:
- 用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用
onCreate()
从头开始重新创建 Activity。 - 系统将应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到
onCreate()
的已保存的实例 state bundle 对于完成此任务有一定助益。
优化方向
我们的优化方向就是 Application和Activity的生命周期 这个阶段,因为这个阶段的时机对于我们来说是可控的。
启动耗时检测
显示所用时间
在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 Displayed
的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。经过的时间包括以下事件序列:
- 启动进程。
- 初始化对象。
- 创建并初始化 Activity。
- 扩充布局。
- 首次绘制应用。
报告的日志行类似于以下示例:
ActivityTaskManager: Displayed com.tianci.localmedia/.MainActivity: +959ms
在所有资源完全加载并显示之前,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
为可选参数。
执行后会得到三个时间:ThisTime、TotalTime和WaitTime,详情如下:
ThisTime
表示最后一个Activity启动耗时。
TotalTime
表示所有Activity启动耗时。
WaitTime
表示AMS启动Activity的总耗时。
一般来说,只需查看得到的TotalTime,即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。
启动速度分析工具 — TraceView
打开 Profiler -> CPU -> 点击 Record -> 点击 Stop -> 查看Profiler下方Top Down/Bottom Up 区域,以找出耗时的热点方法。
检查轨迹
CPU 性能分析器中的轨迹视图提供了多种方法查看来自所记录的轨迹的信息。
对于方法轨迹和函数轨迹,可以直接在 Threads 时间轴中查看 Call Chart,并从 Analysis 窗格中查看 Flame Chart、Top Down、Bottom Up 和 Events 标签页。对于系统轨迹,可以直接在 Threads 时间轴中查看 Trace Events,并从 Analysis 窗格中查看 Flame Chart、Top Down、Bottom Up 和 Events 标签页。
1、Call Chart
Call Chart 以图形方式来呈现方法跟踪数据或函数跟踪数据,其中调用的时间段和时间在横轴上表示,而其被调用方则在纵轴上显示
- 水平轴:表示调用的时间段和时间。
- 垂直轴:显示被调用方。
- 橙色:系统API。
- 绿色:应用自有方法。
- 蓝色:第三方API(包括Java API)。
提示:如需跳转到某个方法或函数的源代码,请右键点击该方法或函数,然后选择 Jump to Source。在“分析”窗格的任意标签页中都可以执行此操作。
2、Flame Chart
Flame Chart 标签页提供一个倒置的调用图表,用来汇总完全相同的调用堆栈。也就是说,将具有相同调用方顺序的完全相同的方法或函数收集起来,并在火焰图中将它们表示为一个较长的横条(而不是将它们显示为多个较短的横条,如调用图表中所示)。这样更方便查看哪些方法或函数消耗的时间最多。不过,这也意味着,横轴不代表时间轴,而是表示执行每个方法或函数所需的相对时间。
- 水平轴:执行每个方法的相对时间量。
- 垂直轴:显示被调用方。
使用技巧
看顶层的哪个函数占据的宽度最大(表现为平顶),可能存在性能问题。
3、Top Down
Top Down 标签显示一个调用列表,在该列表中展开方法或函数节点会显示它的被调用方
如图 所示,在 Top Down 标签页中展开方法 A 的节点会显示它的被调用方,即方法 B 和 D。在此之后,展开方法 D 的节点会显示它的被调用方,即方法 B 和 C,依此类推。与 Flame chart 标签页类似,“Top Down”树也汇总了具有相同调用堆栈的完全相同的方法的跟踪信息。也就是说,Flame chart 标签页提供了 Top down 标签页的图形表示方式。
Top Down 标签提供以下信息来帮助说明在每个调用上所花的 CPU 时间(时间也可表示为在选定范围内占线程总时间的百分比):
- Self:方法或函数调用在执行自己的代码(而非被调用方的代码)上所花的时间。
- Children:方法或函数调用在执行它的被调用方(而非自己的代码)上所花的时间。
- Total:方法的 Self 时间和 Children 时间的总和。这表示应用在执行调用时所用的总时间。
4、Bottom Up
Bottom Up 标签页显示一个调用列表,在该列表中展开函数或方法的节点会显示它的调用方
- 展开函数会显示其调用方。
- 按照消耗CPU时间由多到少的顺序对函数排序。
注意事项
我们在查看上面4个跟踪数据的区域时,应该注意右侧的两个时间,如下所示:
- Wall Clock Time:程序执行时间。
- Thread Time:CPU执行的时间。
5. “Events”表格检查轨迹
“Events”表格列出了当前所选线程中的所有调用。点击列标题对它们进行排序。通过选择表格中的某一行,可以在时间轴上导航到所选调用的开始时间和结束时间。这样就可以在时间轴上准确定位事件。
启动速度分析工具 — Systrace
Systrace 会生成包含多个部分的输出 HTML 文件。该报告列出了每个进程的线程。如果给定线程会渲染界面帧,该报告还会沿时间轴指明所渲染的帧。
使用方式:代码插桩
首先,我们可以定义一个Trace静态工厂类,将Trace.begainSection(),Trace.endSection()封装成i、o方法,然后再在想要分析的方法前后进行插桩即可。
然后,在命令行下执行systrace.py脚本,命令如下所示:
python /Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a "xxxx" -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html
具体参数含义如下:
- -t:指定统计时间为20s。
- shced:cpu调度信息。
- gfx:图形信息。
- view:视图。
- wm:窗口管理。
- am:活动管理。
- app:应用信息。
- webview:webview信息。
- -a:指定目标应用程序的包名。
- -o:生成的systrace.html文件。
报告从上到下包含以下几个部分:
用户互动
第一部分包含表示应用或游戏中的具体用户互动(例如点按设备屏幕)的条形图。这些互动可用作有用的时间标记。
CPU 活动
下一部分显示了表示每个 CPU 中的线程活动的条形图。这些条形会显示所有应用(包括应用或游戏)中的 CPU 活动。
系统事件
此部分中的直方图会显示特定的系统级事件,例如特定对象的纹理计数和总大小。
值得仔细检查的直方图是标记为 SurfaceView 的直方图。计数表示已传递到显示管道并等待显示在设备屏幕上的组合帧缓冲区的数量。由于大多数设备都会进行双重或三重缓冲,因此该计数几乎总为 0、1 或 2。
描绘 Surface Flinger 进程(包括 VSync 事件和界面线程交换工作)的其他直方图。
显示帧
这一部分通常是报告中最顶部的部分,描绘了一条多色线条,后面是成堆的条形。这些形状表示已创建的特定线程的状态和帧堆栈。堆栈的每个层级代表对 beginSection()
的一次调用,或为应用或游戏定义的自定义跟踪事件的开头。
注意:界面线程(即通常运行应用或游戏的主线程)始终会显示为第一个线程。
每个条形堆上方的多色线条表示特定线程随时间变化的一组状态。每段线条可以包含以下颜色之一:
- 绿色:正在运行
线程正在完成与某个进程相关的工作或正在响应中断。 - 蓝色:可运行
线程可以运行但目前未进行调度。 - 白色:休眠
线程没有可执行的任务,可能是因为线程在遇到斥锁定时被阻止。 - 橙色:不可中断的休眠
线程在遇到 I/O 操作时被阻止或正在等待磁盘操作完成。 - 紫色:可中断的休眠
线程在遇到另一项内核操作(通常是内存管理)时被阻止。
注意:在 Systrace 报告中,可以点击该线条以确定该线程在给定时间由哪个 CPU 控制。
具体可参阅系统追踪浏览 Systrace 报告。