应用性能测量概述

本文件可帮助您识别和修复应用中的关键性能问题。

关键性能问题

许多问题都可能导致应用性能下降,但以下是一些常见的需要在您的应用中查找的问题

启动延迟

启动延迟是指从点击应用图标、通知或其他入口点到用户数据显示在屏幕上的时间。

争取在您的应用中实现以下启动目标

  • 冷启动时间少于 500 毫秒。当要启动的应用不在系统内存中时,就会发生 *冷启动*。这种情况发生在应用自重启以来或应用进程被用户或系统停止后的第一次启动时。

    相反,当应用已经在后台运行时,就会发生 *热启动*。冷启动需要系统完成最多的工作,因为它必须从存储加载所有内容并初始化应用。尝试使冷启动时间少于 500 毫秒。

  • P95 和 P99 延迟非常接近中位数延迟。当应用启动时间过长时,会影响用户体验。应用启动关键路径中的进程间通信 (IPC) 和不必要的 I/O 会出现锁争用并导致不一致。

滚动卡顿

*卡顿* 是指系统无法及时构建和提供帧以按请求的 60hz 或更高频率将其绘制到屏幕上时出现的视觉卡顿。滚动时卡顿最为明显,因为动画流程不是平滑的,而是会卡顿。当应用渲染内容的时间超过系统帧持续时间时,移动过程中会在途中暂停一帧或多帧,从而出现卡顿。

应用必须针对 90Hz 刷新率。传统的渲染率为 60Hz,但许多较新的设备在用户交互(例如滚动)期间以 90Hz 模式运行。某些设备甚至支持高达 120Hz 的更高速率。

要查看设备在给定时间使用的刷新率,请使用**开发者选项 > 显示刷新率**(位于**调试**部分)启用叠加层。

不流畅的过渡

这在诸如在标签之间切换或加载新活动之类的交互期间很明显。这些类型的过渡必须是平滑的动画,不能包含延迟或视觉闪烁。

电源效率低

执行工作会降低电池电量,执行不必要的工作会缩短电池寿命。

内存分配(源于代码中创建新对象)可能是系统中大量工作的原因。这是因为分配本身不仅需要 Android 运行时 (ART) 的努力,而且稍后释放这些对象(*垃圾回收*)也需要时间和精力。分配和回收都更快更高效,尤其对于临时对象而言。虽然避免尽可能地分配对象曾经是最佳实践,但我们建议您根据应用程序和架构的需求来选择最合理的方式。鉴于 ART 的能力,以牺牲代码可维护性为代价节省分配并不是最佳实践。

但是,这需要付出努力,因此请记住,如果您在内部循环中分配许多对象,这可能会导致性能问题。

识别问题

我们建议使用以下工作流程来识别和解决性能问题

  1. 识别并检查以下关键用户旅程
    • 常见的启动流程,包括从启动器和通知启动。
    • 用户滚动浏览数据的屏幕。
    • 屏幕之间的转换。
    • 长时间运行的流程,例如导航或音乐播放。
  2. 使用以下调试工具检查上述流程期间发生的情况
    • Perfetto:允许您查看整个设备上发生的情况以及精确的计时数据。
    • 内存分析器:允许您查看堆上发生的内存分配。
    • Simpleperf:显示特定时间段内哪些函数调用使用了最多 CPU 的火焰图。当您在 Systrace 中识别出耗时很长的内容,但不知道原因时,Simpleperf 可以提供更多信息。

要理解和调试这些性能问题,手动调试单个测试运行至关重要。您不能通过分析汇总数据来替代上述步骤。但是,为了了解用户实际看到的内容并识别何时可能发生回归,务必在自动化测试和现场设置指标收集。

  • 启动流程
  • 卡顿
    • 现场指标
      • Play Console 帧重要指标:在 Play Console 中,您无法将指标缩小到特定用户旅程。它仅报告应用程序中的整体卡顿情况。
      • 使用 FrameMetricsAggregator 进行自定义测量:您可以使用 FrameMetricsAggregator 在特定工作流程期间记录卡顿指标。
    • 实验室测试
      • 使用 Macrobenchmark 滚动.
      • Macrobenchmark 使用括起单个用户旅程的 dumpsys gfxinfo 命令收集帧计时。这是一种了解特定用户旅程中卡顿变化的方法。RenderTime 指标(突出显示绘制帧需要多长时间)比卡顿帧计数更重要,因为它可以用于识别回归或改进。

应用链接 是基于您的网站 URL 的深度链接,已验证属于您的网站。以下是一些可能导致应用链接验证失败的原因。

  • 意图过滤器范围:仅为您的应用可以响应的 URL 添加 autoVerify 到意图过滤器。
  • 未验证的协议切换:未验证的服务器端和子域重定向被视为安全风险,并且验证会失败。它们会导致所有 autoVerify 链接失败。例如,在未验证 HTTPS 链接的情况下,将链接从 HTTP 重定向到 HTTPS(例如,example.com 重定向到 www.example.com)会导致验证失败。请务必通过添加意图过滤器来 验证应用链接
  • 不可验证的链接:出于测试目的添加不可验证的链接可能会导致系统无法验证您应用的应用链接。
  • 不可靠的服务器:确保您的服务器可以连接到您的客户端应用。

设置应用以进行性能分析

必须正确设置才能从应用中获得准确、可重复、可操作的基准测试。在尽可能接近生产环境的系统上进行测试,同时抑制噪声源。以下部分显示了您可以采取的一些 APK 和系统特定步骤来准备测试设置,其中一些步骤是特定于用例的。

跟踪点

应用可以使用 自定义跟踪事件 来检测其代码。

在捕获跟踪时,跟踪会产生大约每部分 5μs 的少量开销,因此不要将其放在每个方法周围。跟踪大于 0.1ms 的较大部分工作可以深入了解瓶颈。

APK 注意事项

调试变体有助于排除故障和符号化堆栈样本,但它们会严重影响性能。运行 Android 10(API 级别 29)及更高版本的设备可以在其清单中使用 profileable android:shell="true" 来启用发行版本中的分析。

使用您的生产级 代码缩减 配置。根据您的应用使用的资源,这可能会对性能产生重大影响。某些 ProGuard 配置会删除跟踪点,因此请考虑删除您正在运行测试的配置的这些规则。

编译

编译 设备上的应用到已知状态,通常是 speedspeed-profile。后台即时 (JIT) 活动可能会产生大量的性能开销,如果您在测试运行之间重新安装 APK,则经常会遇到这种情况。执行此操作的命令如下:

adb shell cmd package compile -m speed -f com.google.packagename

speed 编译模式会完全编译应用。speed-profile 模式会根据在应用使用期间收集的已用代码路径配置文件来编译应用。一致且正确地收集配置文件可能很困难,因此如果您决定使用它们,请确认它们正在收集您期望的内容。配置文件位于以下位置:

/data/misc/profiles/ref/[package-name]/primary.prof

Macrobenchmark 允许您直接 指定编译模式

系统注意事项

对于低级别和高保真度测量,请校准您的设备。在同一设备和同一操作系统版本上运行 A/B 比较。即使在同一设备类型之间,性能也可能存在显著差异。

在已植根的设备上,请考虑为 Microbenchmark 使用 lockClocks 脚本。除其他事项外,这些脚本执行以下操作:

  • 将 CPU 设置为固定频率。
  • 禁用小核心并配置 GPU。
  • 禁用热节流。

我们不建议将 lockClocks 脚本用于以用户体验为中心的测试(例如应用启动、DoU 测试和卡顿测试),但对于减少 Microbenchmark 测试中的噪声至关重要。

如果可能,请考虑使用 Macrobenchmark 等测试框架,它可以减少测量中的噪声并防止测量不准确。

应用启动缓慢:不必要的跳板活动

跳板活动可能会不必要地延长应用启动时间,了解您的应用是否正在执行此操作非常重要。如下例跟踪所示,一个 activityStart 紧跟在另一个 activityStart 之后,而第一个活动没有绘制任何帧。

alt_text 图 1. 显示跳板活动的跟踪图。

这在通知入口点和常规应用启动入口点中都会发生,您通常可以通过重构来解决此问题。例如,如果您使用此活动在另一个活动运行之前执行设置,请将此代码分解为可重用的组件或库。

不必要的分配触发频繁的 GC

您可能会在 Systrace 中看到垃圾回收 (GC) 比您预期的更频繁地发生。

在以下示例中,长时间运行的操作期间每 10 秒发生一次,这表明应用可能在一段时间内不必要但持续地进行分配。

alt_text 图 2. 显示 GC 事件之间间隔的跟踪图。

使用内存分析器时,您还可能会注意到特定调用堆栈正在进行绝大多数分配。您无需积极消除所有分配,因为这会使代码难以维护。相反,从处理分配热点开始。

卡顿帧

图形管道相对复杂,确定用户最终是否会看到丢帧可能会有一些细微之处。在某些情况下,平台可以使用缓冲来“抢救”帧。但是,您可以忽略大部分细微之处,从应用的角度识别有问题的帧。

当绘制帧时,应用几乎不需要任何工作,Choreographer.doFrame() 跟踪点在 60 FPS 设备上以 16.7ms 的节奏出现。

alt_text 图 3. 显示频繁快速帧的跟踪图。

如果您缩小并浏览跟踪,有时会看到帧完成需要的时间稍长一些,但这仍然是可以的,因为它们并没有超过分配的 16.7ms 时间。

alt_text 图 4. 显示频繁快速帧和周期性工作突发的跟踪图。

当您看到这种规律节奏的中断时,就是一个卡顿帧,如图 5 所示。

alt_text 图 5. 显示卡顿帧的跟踪图。

您可以练习识别它们。

alt_text 图 6. 显示更多卡顿帧的跟踪图。

在某些情况下,您需要放大跟踪点以获取有关正在膨胀哪些视图或RecyclerView 正在执行哪些操作的更多信息。在其他情况下,您可能需要进一步检查。

有关识别卡顿帧和调试其原因的更多信息,请参阅 渲染缓慢

常见的 RecyclerView 错误

不必要地使 RecyclerView 的整个后备数据失效会导致帧渲染时间过长和卡顿。相反,为了最大限度地减少需要更新的视图数量,只需使更改的数据失效。

请参阅 显示动态数据,了解如何避免代价高昂的 notifyDatasetChanged() 调用,这些调用会导致内容更新而不是完全替换内容。

如果您没有正确支持每个嵌套的 RecyclerView,则会导致内部 RecyclerView 每次都完全重新创建。每个嵌套的内部 RecyclerView 都必须设置 RecycledViewPool,以帮助确保可以在每个内部 RecyclerView 之间回收视图。

预取数据不足或预取时机不当,会导致用户滚动列表到达底部时需要等待服务器返回更多数据,从而产生卡顿感。虽然这在技术上不算卡顿(没有错过任何帧期限),但通过修改预取的时间和数量,避免用户等待数据,可以显著提升用户体验。

调试你的应用

以下是调试应用性能的不同方法。请观看以下视频,了解系统跟踪和使用 Android Studio Profiler 的概述。

使用 Systrace 调试应用启动

有关应用启动过程的概述,请参阅应用启动时间,并观看以下视频,了解系统跟踪的概述。

你可以在以下阶段区分启动类型

  • 冷启动:从创建一个没有保存状态的新进程开始。
  • 温启动:重新创建活动时重用进程,或使用保存状态重新创建进程。
  • 热启动:重新启动活动,从 inflation 开始。

我们建议使用设备上的系统跟踪应用捕获 Systrace。对于 Android 10 及更高版本,请使用Perfetto。对于 Android 9 及更低版本,请使用Systrace。我们还建议使用基于 Web 的 Perfetto 追踪查看器查看跟踪文件。更多信息,请参阅系统跟踪概述

一些需要注意的事项包括:

  • 监控竞争:对受监视器保护的资源的竞争可能会导致应用启动的显著延迟。
  • 同步 Binder 事务:查找应用关键路径中不必要的事务。如果必要的事务开销很大,请考虑与相关的平台团队合作以进行改进。

  • 并发 GC:这很常见,影响相对较小,但如果经常遇到这种情况,请考虑使用 Android Studio 内存分析器进行检查。

  • I/O:检查启动期间执行的 I/O,并查找长时间停顿。

  • 其他线程上的大量活动:这些可能会干扰 UI 线程,因此请注意启动期间的后台工作。

我们建议你在应用层面完成启动后调用reportFullyDrawn,以改进应用启动指标的报告。有关使用reportFullyDrawn 的更多信息,请参阅完全显示时间部分。你可以通过 Perfetto 跟踪处理器提取 RFD 定义的启动时间,并且会发出用户可见的跟踪事件。

使用设备上的系统跟踪

你可以使用名为系统跟踪的系统级应用来在设备上捕获系统跟踪。此应用允许你录制设备中的跟踪,无需将其插入或连接到adb

使用 Android Studio 内存分析器

你可以使用Android Studio 内存分析器检查可能由内存泄漏或不良使用模式引起的内存压力。它提供对象的实时分配视图。

你可以通过使用内存分析器跟踪 GC 发生的原因和频率来修复应用中的内存问题。

要分析应用内存,请执行以下步骤:

  1. 检测内存问题。

    记录你想要关注的用户旅程的内存分析会话。查找对象数量增加的情况(如图 7 所示),最终会导致 GC(如图 8 所示)。

    alt_text 图 7. 对象数量增加。

    alt_text 图 8. 垃圾回收。

    确定导致内存压力的用户旅程后,分析内存压力根本原因。

  2. 诊断内存压力热点。

    选择时间轴中的一个范围来可视化分配浅层大小(如图 9 所示)。

    alt_text 图 9. 分配浅层大小的值。

    有多种方法可以对这些数据进行排序。以下是一些关于每个视图如何帮助你分析问题的示例。

    • 按类排列:当你想要查找生成对象(否则这些对象会被缓存或从内存池中重用)的类时很有用。

      例如,如果你看到一个应用每秒创建 2000 个名为“Vertex”的类对象,它会使分配计数每秒增加 2000,并且在按类排序时你会看到它。如果要重用这些对象以避免生成垃圾,请实现内存池。

    • 按调用栈排列:当你想要查找内存分配的热点路径(例如循环内或执行大量分配工作的特定函数内)时很有用。

    • 浅层大小:仅跟踪对象本身的内存。它对于跟踪主要由原始值组成的简单类很有用。

    • 保留大小:显示由于对象及其仅由该对象引用的引用而产生的总内存。它对于跟踪由复杂对象引起的内存压力很有用。要获取此值,请执行完整的内存转储(如图 10 所示),并将保留大小添加为列(如图 11 所示)。

      alt_text 图 10. 完整的内存转储。

      Retained Size column.
      图 11. 保留大小列。
  3. 衡量优化的影响。

    GC 更明显,更容易衡量内存优化的影响。当优化减少内存压力时,你会看到更少的 GC。

    要衡量优化的影响,请在分析器时间轴中测量 GC 之间的时间。然后你可以看到 GC 之间的时间变长了。

    内存改进的最终影响如下:

    • 如果应用不会持续遇到内存压力,则可能会减少内存不足导致的关闭。
    • 减少 GC 次数可以改善卡顿指标,尤其是在 P99 时。这是因为 GC 会导致 CPU 竞争,这可能会导致渲染任务在 GC 发生时被延迟。