衡量应用性能概述

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

关键性能问题

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

启动延迟

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

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

  • 冷启动时间少于 500 毫秒。冷启动是指在系统内存中不存在要启动的应用时发生的启动。这种情况发生在应用自重启以来或自应用进程被用户或系统停止以来首次启动时。

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

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

滚动卡顿

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

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

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

动画不流畅

这在诸如切换标签或加载新 Activity 之类的交互过程中很明显。这些类型的过渡必须是流畅的动画,并且不包含延迟或视觉闪烁。

功耗效率低

执行操作会消耗电池电量,而执行不必要的操作则会缩短电池续航时间。

内存分配(源于在代码中创建新对象)可能是系统中大量工作的原因。这是因为分配本身需要 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 比较。即使在同一设备类型之间,性能也可能存在很大差异。

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

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

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

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

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

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

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

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

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

您可能会在 Systrace 中看到垃圾回收 (GC) 的频率超出预期。

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

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 分析器的概述。

使用 Systrace 调试应用启动

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

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

  • 冷启动:从创建新进程开始,且没有保存的状态
  • 温启动:重新创建活动时重用进程,或重新创建进程并使用保存的状态。
  • 热启动:重新启动活动并从加载布局开始。

我们建议使用设备上的系统跟踪应用捕获 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 进行时被延迟。