本主题可帮助您找出并修复应用中的关键性能问题。
关键性能问题
有许多问题会导致应用性能变差,但您应特别留意应用中的以下一些常见情形:
- 滚动卡顿
- “卡顿”一词是指在以下情况下出现的画面停顿:系统无法及时构建和提供帧,以致无法以请求的频率(60hz 或更高)将其绘制到屏幕上。卡顿问题在滚动操作期间最为明显。此时,由于应用渲染内容所需的时间超过了帧在系统上显示的时长,本应流畅播放动画的过程会出现停滞,动作停顿一帧或多帧的时长。
- 应将应用的刷新频率目标定在 90Hz。以前的渲染速率是 60Hz,但许多新型设备在滚动之类的用户互动期间会以 90Hz 的模式运行,还有些设备甚至可支持更高的频率,最高可达 120Hz。
- 如需查看设备在特定时间所用的刷新频率,请使用 Debugging 部分的 Developer Options > Show refresh rate 启用叠加层。
-
- 启动延迟时间是指从点按应用图标、通知或其他入口点到屏幕上显示用户的数据所需的时间。
您应当在您的应用中实现以下两个启动目标:
冷启动 < 500 毫秒:当启动的应用不在系统的内存中时,即为“冷启动”。应用在系统重启后或应用进程被用户或系统终止后首次启动时,就会进行冷启动。
相反的,如果应用已在后台运行,则为“温启动”。冷启动需要系统完成大部分工作,因为系统必须从存储空间加载所有内容并初始化应用。请将冷启动时间的目标设为 500 毫秒或更短时间。
P95/P99 延迟时间应非常接近延迟时间中值。如果应用有时需要很长时间才能启动,就会损害用户的信任。应用启动关键路径中的 IPC 和不必要的 I/O 可能会遇到锁争用,进而造成这些不一致的情况。
转换不顺畅
- 此类问题出在互动期间,例如在标签页之间切换或加载新的 activity 时。此类转换的动画应当自然流畅,没有延迟或画面抖动。
电源效率低下
- 执行工作会消耗电量,执行不必要的工作会缩短电池续航时间。
因在代码中创建新对象而导致的内存分配可能是致使系统中产生大量工作的原因。这是因为,不仅内存分配本身需要在 Android 运行时中完成工作,稍后释放这些对象(“垃圾回收”)也需要耗费时间和工作量。如今,分配和回收的速度与效率都远超以往,尤其是对临时对象而言。因此,对于过去要求尽可能避免分配对象的情况,现在会建议执行最适合应用和架构的操作;考虑到 ART 的能力,冒着无法维护代码的风险来减少分配并不是正确的选择。
不过,这还是需要一些工作量,因此您仍需注意是否在内部循环中分配了许多对象,这种做法可能会导致性能问题。
发现问题
建议通过以下工作流发现和解决性能问题:
- 确定要检查的关键用户历程。这些工作流可能包括:
- 常用的启动工作流,包括从启动器启动和从通知启动。
- 用户滚动浏览其中数据的任何屏幕。
- 在屏幕之间切换。
- 长时间运行的工作流,例如导航或播放音乐。
- 使用调试工具检查这些工作流中发生的情况:
- Systrace 或 Perfetto:可以让您根据准确的计时数据,确切了解整个设备中发生的操作。
- 内存性能分析器:可以让您了解堆上正在发生的内存分配。
- Simpleperf:查看有关特定时间段内哪些函数调用占用最多 CPU 的火焰图。当您在 systrace 中发现有些操作用时很长但又不知道什么原因时,Simpleperf 可以提供额外的信息。
手动调试各个测试运行对于了解和调试这些性能问题至关重要。分析汇总数据不能取代上述步骤。不过,若想了解用户实际看到的内容并确定可能发生性能下降的情况,为自动测试以及实际运行设置指标收集也同样重要:
- 启动工作流
- 实际运行指标:Play 管理中心启动时间
- 实验室测试 Jetpack Macrobenchmark:启动
- 卡顿
- 实际运行指标
- Play 管理中心框架要点:请注意,在 Play 管理中心内,不可能将指标的收集范围缩小到某个特定的用户体验历程,因为报告的所有数据都是整个应用的整体卡顿情况。
- 使用
FrameMetricsAggregator
进行自定义测量:您可以使用FrameMetricsAggregator
记录特定工作流期间的卡顿指标。
- 实验室测试
- Jetpack Macrobenchmark:滚动
- Macrobenchmark 使用只包含单个用户体验历程的
dumpsys gfxinfo
命令来收集帧时间。这是了解卡顿问题在特定用户体验历程期间的变化的合理方式。RenderTime
指标重点衡量绘制帧所需的时间,在发现性能下降问题或确定改进措施方面,该指标比“卡顿帧数量”更重要。
- 实际运行指标
设置应用以进行性能分析
正确的设置对于从应用获取准确、可重复、可操作的基准测试结果至关重要。请在尽可能接近生产环境的系统上进行测试,同时抑制噪声来源。下面几部分将介绍一些 APK 和系统特有的测试设置准备步骤,其中部分是特定于用例的步骤。
跟踪点
应用可以利用自定义跟踪事件在代码中插桩。
在捕获跟踪记录时,每个部分的跟踪确实会产生少量开销(约 5 微秒),因此请勿在每个方法中都使用跟踪。仅跟踪较大的工作块(> 0.1 毫秒)可以提供关于瓶颈的重要洞察。
APK 注意事项
注意:请勿在调试 build 上测量性能。
调试变体对于排查堆栈样本的问题和对其进行符号化处理非常实用,但它们会对性能产生严重的非线性影响。搭载 Android 10(API 级别 29)及更高版本的设备可以在其清单中使用 profileable android:shell="true"
,以在发布 build 中启用性能剖析。
使用生产级代码缩减配置。根据应用所使用的资源,这可能会对性能产生重大影响。请注意,某些 ProGuard 配置会移除跟踪点,因此请考虑针对要运行测试的配置移除这些规则。
编译
将设备上的应用编译为已知状态(通常是 speed 或 speed-profile)。后台 JIT activity 的性能开销可能会很高,而且如果您在两次运行测试之间重新安装 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 权限的设备上,请考虑使用 lockClocks
脚本进行微基准测试。除发挥其他作用外,这些脚本还会执行以下操作:
- 将 CPU 设为固定频率。
- 停用小核心,配置 GPU。
- 停用温控调频。
这种办法不适合以用户体验为重点的测试(例如应用启动、DoU 测试和卡顿测试),但对减少微基准测试中的噪声至关重要。
如有可能,请考虑使用 Microbenchmark 等测试框架,因为此类测试框架可以减少测量中的噪声并防止测量结果不准。
应用启动缓慢:不必要的 trampoline activity
trampoline activity 会不必要地延长应用启动时间,因此您必须弄清楚您的应用是否存在这种情况。如以下示例跟踪记录所示,一个 activityStart
紧跟在另一个 activityStart
之后,前一个 activity 根本来不及绘制任何帧。
这种情况在通知入口点和常规应用启动入口点都有可能发生,而且通常可以通过重构解决问题。例如,如果您要使用该 activity 在另一个 activity 运行之前执行设置,请将该代码分解到可重复使用的组件或库中。
触发频繁 GC 的不必要分配
您可能会在 systrace 中发现,垃圾回收 (GC) 的频率高于您的预期。
在这种情况下,如果在长时间运行的操作期间每 10 秒就出现一次垃圾回收,表示您的应用可能一直在不必要地持续进行分配:
或者,您可能会在使用内存性能分析器时发现,绝大部分的分配都源于某个特定的调用堆栈。您不需要激进地消除所有分配,因为这样做可能会使代码维护起来更加困难。不妨改为从分配热点着手。
卡顿帧
图形流水线相对复杂一些,在确定用户最终是否会看到丢失的帧方面可能存在一些细微差别;在某些情况下,平台可能会使用缓冲来“救援”帧。不过,您可以忽略其中的大部分细微差别,从应用的角度轻松发现有问题的帧。
在绘制帧时几乎不需要应用完成什么工作的情况下,Choreographer.doFrame()
跟踪点的间隔为 16.7 毫秒(假设设备的帧速率为 60 帧/秒):
如果缩小并浏览跟踪记录,有时会看到一些帧需要稍长时间才能完成,但这种情况仍然没有问题,因为这些帧的用时并未超过分配给它们的 16.7 毫秒时间:
当您确实在这种规律的间隔中看到一处中断时,那才是一个卡顿帧:
只需稍加练习,您就能轻松看出卡顿帧。
在某些情况下,您需要放大跟踪点来详细了解膨胀了哪些视图或 RecyclerView 正在执行什么工作。在其他情况下,您可能必须进一步检查。
如需详细了解如何发现卡顿帧并调试其原因,请参阅渲染速度缓慢。
常见的 RecyclerView 错误
- 不必要地让整个
RecyclerView
的后备数据失效。这可能会导致帧的渲染时间较长,进而造成卡顿。您应当仅让已更改的数据失效,从而最大限度地减少需要更新的视图数量。- 请参阅呈现动态数据,了解避免高开销
notifyDatasetChanged()
调用的方式,这些方式会更新内容而不是完全替换内容。
- 请参阅呈现动态数据,了解避免高开销
- 无法正确支持嵌套的
RecyclerView
,导致每次都会完全重新创建内部RecyclerView
。- 每个嵌套的内部
RecyclerView
都应设置一个RecycledViewPool
,用于确保可在内部RecyclerView
之间循环使用视图。
- 每个嵌套的内部
- 未预提取足够的数据,或未及时预提取数据。快速到达滚动列表底部但需要等待从服务器提取更多数据可能会令人不快。虽然从技术角度而言这并不属于“卡顿”,因为并没有错过任何帧的截止时间,但如果能修改预提取的时间和数量,让用户不必等待数据,用户体验就会得到显著提升。