衡量应用性能概览

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

主要性能问题

许多问题都可能导致应用性能不佳,但以下是您应用中需要注意的一些常见问题:

启动延迟

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

您的应用应力求达到以下启动目标:

  • 冷启动时间少于 500 毫秒。当系统内存中不存在所启动的应用时,就会发生 冷启动。这发生在应用自重启或应用进程被用户或系统停止后的首次启动时。

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

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

滚动卡顿

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

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

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

不流畅的过渡

这在标签页切换或加载新 Activity 等互动期间很明显。这类过渡必须是流畅的动画,不应包含延迟或视觉闪烁。

电量效率低下

执行工作会减少电池电量,执行不必要的工作会缩短电池续航时间。

内存分配(来自在代码中创建新对象)可能是系统中大量工作的原因。这不仅因为分配本身需要 Android Runtime (ART) 的工作,而且稍后释放这些对象(垃圾回收)也需要时间和精力。分配和回收都快得多、效率高得多,特别是对于临时对象。虽然过去最佳实践是尽可能避免分配对象,但我们建议您选择对您的应用和架构最有意义的做法。考虑到 ART 的能力,以难以维护的代码为代价来节省分配并非最佳实践。

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

识别问题

我们推荐以下工作流程来识别和解决性能问题:

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

要理解和调试这些性能问题,手动调试单个测试运行至关重要。您无法通过分析聚合数据来替代上述步骤。但是,要了解用户实际看到的内容并确定何时可能出现回归,在自动化测试和现场设置指标收集非常重要:

  • 启动流程
  • 卡顿
    • 现场指标
      • Play 管理中心帧健康指标:在 Play 管理中心内,您无法将指标缩小到特定的用户旅程。它仅报告整个应用中的总体卡顿。
      • 使用 FrameMetricsAggregator 进行自定义测量:您可以使用 FrameMetricsAggregator 在特定工作流中记录卡顿指标。
    • 实验室测试
      • 使用 Macrobenchmark 滚动.
      • Macrobenchmark 使用 dumpsys gfxinfo 命令收集帧时间,这些命令将单个用户旅程括起来。这是一种了解特定用户旅程中卡顿变化的方式。RenderTime 指标(强调绘制帧所需的时间)比卡顿帧的数量更重要,用于识别回归或改进。

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

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

设置应用以进行性能分析

正确设置对于从应用获取准确、可重复、可操作的基准至关重要。在尽可能接近生产的环境中进行测试,同时抑制噪音源。以下部分介绍了您可以采取的许多 APK 和系统特定步骤来准备测试设置,其中一些是特定于用例的。

跟踪点

应用可以使用自定义跟踪事件对其代码进行插桩。

在捕获跟踪数据时,跟踪会产生很小的开销,大约每部分 5 微秒,因此不要将其放置在每个方法周围。跟踪大于 0.1 毫秒的较大工作块可以为瓶颈提供重要见解。

APK 考量因素

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

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

编译

将您的应用在设备上编译到已知状态 - 通常为 speed 以简化,或为 speed-profile 以更接近生产性能(尽管这需要预热应用程序和转储配置文件,或编译应用程序的基线配置文件)。

speedspeed-profile 都会减少从 dex 解释运行的代码量,从而减少可能导致显著干扰的后台即时 (JIT) 编译量。只有 speed-profile 会减少运行时从 dex 加载类产生的影响。

以下命令使用 speed 模式编译应用程序:

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

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

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

系统考量

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

在已 root 的设备上,请考虑对微基准使用 lockClocks 脚本。除其他外,这些脚本执行以下操作:

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

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

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

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

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

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

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

不必要的分配导致频繁 GC

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

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

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

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

卡顿帧

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

当帧绘制所需的应用工作量很少时,在 60 FPS 设备上,Choreographer.doFrame() 跟踪点以 16.7 毫秒的节奏发生:

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

如果您缩小并浏览跟踪,有时会看到帧完成时间稍长,但仍然可以,因为它们没有超过分配的 16.7 毫秒时间:

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

当您看到这种常规节奏被中断时,它就是一个卡顿帧,如图 5 所示:

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

您可以练习识别它们。

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

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

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

常见的 RecyclerView 错误

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

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

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

预取数据不足或未及时预取数据,可能会在用户需要等待服务器提供更多数据时,导致滚动列表底部出现突兀感。尽管这在技术上并非卡顿(因为没有错过帧截止时间),但通过修改预取的时间和数量,让用户不必等待数据,可以显著改善用户体验。

调试您的应用

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

使用 Systrace 调试应用启动

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

您可以在以下阶段消除启动类型的歧义:

  • 冷启动:从创建没有保存状态的新进程开始。
  • 热启动:在重用进程的同时重新创建 Activity,或在保存状态下重新创建进程。
  • 暖启动:重新启动 Activity 并从膨胀开始。

我们建议使用设备上的系统跟踪应用捕获 Systrace。对于 Android 10 及更高版本,请使用 Perfetto。对于 Android 9 及更低版本,请使用 Systrace。我们还建议使用基于网络的 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. 分配浅层大小的值。

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

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

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

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

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

    • 保留大小:显示由于对象和仅由对象引用的引用而产生的总内存。它对于跟踪复杂对象导致的内存压力很有用。要获取此值,请进行完整的内存转储,如图 10 所示,并且保留大小将作为一列添加,如图 11 所示。

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

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

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

    要衡量优化的影响,请在分析器时间轴中测量 GC 之间的时间。然后您会发现 GC 之间的时间间隔更长。

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

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