案例研究:Gmail Wear OS 团队如何将应用启动速度提升 50%

应用启动代表了应用给用户的第一印象。用户不喜欢等待,因此您需要确保应用启动速度快。为了向您展示现实应用开发团队如何查找和诊断应用启动问题,以下是 Gmail Wear OS 团队所做的事情。

Gmail Wear OS 团队开展了优化工作,重点关注应用启动和运行时渲染性能,以满足其团队的应用性能标准。但是,即使您没有特定的阈值要针对,如果您花一些时间调查,几乎总是有改进应用启动的空间。

捕获跟踪并查看应用启动

要开始分析,捕获跟踪,其中包含应用启动,以便在 Perfetto 或 Android Studio 中进行更仔细的检查。本案例研究使用 Perfetto,因为它会向您展示设备系统中发生的事情,而不仅仅是您的应用。当您在 Perfetto 中上传跟踪时,它看起来像这样

图 1. Perfetto 中跟踪的初始视图。

由于重点是改进应用启动,因此找到包含 Android 应用启动 自定义指标的行;通过单击悬停在行上时出现的固定图标 ,可以将其固定到视图顶部。您在 Android 应用启动 行上看到的条形或切片 指示了应用启动覆盖的时间范围,直到第一个应用帧绘制到屏幕上,因此您应该查找该区域中的问题或瓶颈。

Android App Startups row with option to pin highlighted.
图 2. 将 Android 应用启动自定义指标固定到仪表板顶部,以便更容易进行分析。

请注意,Android 应用程序启动 指标代表的是 初始显示时间,即使您使用的是 reportFullyDrawn()。要识别 完全显示时间,请在 Perfetto 搜索框中搜索 reportFullyDrawn()

检查主线程

首先,检查 主线程 上发生了什么。主线程非常重要,因为它通常是所有 UI 渲染发生的地方;当它被阻塞时,不会发生任何绘制,并且您的应用程序看起来像冻结了一样。因此,您需要确保在主线程上不会发生任何长时间运行的操作。

要找到主线程,请找到包含您的应用程序包名称的行并展开它。与包名称相同的两行(通常是该部分中的前两行)表示主线程。在这两行主线程中,第一行表示 CPU 状态,第二行表示跟踪点。将这两行主线程固定在 Android 应用程序启动 指标下方。

Android App Startups and main thread rows pinned.
图 3. 将主线程行固定在 Android 应用程序启动自定义指标下方以帮助分析。

在可运行状态和 CPU 争用中花费的时间

要获得应用程序启动期间 CPU 活动的汇总视图,请将光标拖到主线程上以捕获应用程序启动时间范围。出现的 线程状态 面板将向您显示在您选择的时间范围内每个 CPU 状态中花费的总时间。

查看在 Runnable 状态中花费的时间。当线程处于 Runnable 状态时,线程可用于执行工作,但没有安排任何工作。这可能表明设备负载过重,无法安排高优先级任务。最上面的用户可见应用程序在调度中具有最高优先级,因此空闲的主线程通常表明应用程序内的密集进程(例如动画渲染)正在与主线程竞争 CPU 时间。

Main thread highlighted with total time in different states in Thread States panel.
图 4. 评估 Runnable 状态与 Running 状态之间的相对时间,以初步了解 CPU 争用程度。

Runnable 状态中花费的时间与在 Running 状态中花费的时间的比率越高,发生 CPU 争用的可能性就越大。当以这种方式检查性能问题时,请先关注运行时间最长的帧,然后逐步处理运行时间更短的帧。

当分析在 Runnable 状态中花费的时间时,请考虑设备硬件。由于所描绘的应用程序运行在具有两个 CPU 的可穿戴设备上,因此预计与查看具有更多 CPU 的设备相比,在 Runnable 状态中花费的时间更多,并且与其他进程的 CPU 争用也更多。即使在 Runnable 状态中花费的时间比典型手机应用程序预期的时间更多,在可穿戴设备的环境下也可能是可以理解的。

OpenDexFilesFromOat* 中花费的时间

现在检查在 OpenDexFilesFromOat* 中花费的时间;在跟踪中,它与 bindApplication 片段同时发生。该片段表示读取应用程序 DEX 文件所花费的时间。

被阻塞的 Binder 事务

接下来检查 Binder 事务。Binder 事务表示客户端和服务器之间的调用:在本例中,应用程序(客户端)使用 Binder 事务 调用 Android 系统(服务器),服务器使用 Binder 回复 进行响应。请确保应用程序在启动期间不会执行不必要的 Binder 事务,因为它们会增加 CPU 争用的风险。如果可以,请将涉及 Binder 调用的工作推迟到应用程序启动阶段之后。如果您必须执行 Binder 事务,请确保它们不会超过设备的 Vsync 刷新率

第一个 Binder 事务(通常在 ActivityThreadMain 片段同时发生)在本例中似乎很长。要了解有关可能发生的事情的更多信息,请执行以下步骤

  1. 要查看相关的 Binder 回复并了解有关 Binder 事务优先级的更多信息,请单击您感兴趣的 Binder 事务片段。
  2. 要查看 Binder 回复,请转到 当前选择 面板并单击 后续线程 部分下的 Binder 回复。如果您想手动导航到该位置,线程 字段还会告诉您 Binder 回复发生的线程;它将在不同的进程中。将出现一条连接 Binder 事务和回复的线。

    A line connects the binder call and reply.
    图 5. 识别应用程序启动期间发生的 Binder 事务,并评估是否可以推迟它们。
  3. 要查看系统服务器如何处理此 Binder 事务,请将 Cpu 0Cpu 1 线程固定到屏幕顶部。

  4. 通过找到包含 Binder 回复线程名称(在本例中为 "Binder:687_11 [2542]")的片段来查找处理 Binder 回复的系统进程。单击相关的系统进程以获取有关 Binder 事务的更多信息。

查看与在 CPU 0 上发生的您感兴趣的 Binder 事务相关的此系统进程

System process with End State 'Runnable (Preempted).
图 6. 系统进程处于 Runnable (Preempted) 状态,这表明它正在被延迟。

结束状态 显示 Runnable (Preempted),这意味着该进程正在被延迟,因为 CPU 正在执行其他操作。要找出它被什么抢占了,请展开 Ftrace 事件 行。在可用的 Ftrace 事件 选项卡中,滚动浏览并查找与您感兴趣的 Binder 线程 "Binder:687_11 [2542]" 相关的事件。在系统进程被抢占的大致时间内,发生了两个包含参数 "decon" 的系统服务器事件,这意味着它们与显示控制器相关。这听起来很合理,因为显示控制器将帧放到屏幕上——一项重要的任务!因此,请保留这些事件。

FTrace events associated with the binder transaction of interest highlighted.
图 7. FTrace 事件表明 Binder 事务正在被显示控制器事件延迟。

JIT 活动

要调查 即时编译 (JIT) 活动,请展开属于您的应用程序的进程,找到两个 "Jit 线程池" 行,并将它们固定到视图的顶部。由于此应用程序在应用程序启动期间受益于 基线配置文件,因此在绘制第一帧之前几乎不会发生任何 JIT 活动,这由第一个 Choreographer.doFrame 调用的结束标志。

Jit thread pools with 'Jit compiling void' slice highlighted.
图 8. 如果您看到很多 JIT 活动,请将基线配置文件扩展到应用程序可以使用的位置。

结果

由于进行了此分析,Gmail Wear OS 团队做出了以下改进

  • 由于他们注意到在查看 CPU 活动 时应用程序启动期间的争用,因此他们用一个静态图像替换了用于指示应用程序正在加载的微调器动画。他们还延长了启动画面,以推迟闪烁状态(用于指示应用程序正在加载的第二个屏幕状态),以释放 CPU 资源。这使应用程序启动延迟提高了 50%。
  • 从查看在 OpenDexFilesFromOat*JIT 活动 中花费的时间来看,他们启用了 R8基线配置文件 的重写。这使应用程序启动延迟提高了 20%。

以下是该团队关于如何有效分析应用程序性能的一些提示

  • 建立一个能够自动收集跟踪和结果的持续流程。考虑使用 基准测试 为您的应用程序设置自动跟踪。
  • 对您认为会改进应用程序的更改进行 A/B 测试,如果更改没有改进,请拒绝它们。您可以使用 Macrobenchmark 库在不同的情况下测量性能。

要了解更多信息,请查看以下资源