Flutter 性能分析
诊断 Flutter 中的 UI 性能问题。
概述
#应用性能涵盖了多个方面,从原始速度和 I/O 吞吐量到用户界面的流畅度。虽然本页主要关注 UI 流畅度(避免卡顿或掉帧),但此处介绍的工具通常也可用于诊断其他性能问题。
Flutter 提供了多种性能分析工具。以下是其中的一些:
-
性能图层 (Performance Overlay):直接在运行的应用内显示一组简化的指标。要了解更多信息,请参阅本主题中的相关章节。
-
性能视图 (Performance View):一个基于 Web 的界面,可连接到你的应用并显示详细的性能指标。它是 DevTools 工具集的一部分。要了解更多信息,请参阅 使用性能视图。
-
Dart 中的性能追踪:使用
dart:developer包直接在应用的 Dart 代码中添加追踪,然后在 DevTools 工具中跟踪应用的性能。要了解更多信息,请参阅 追踪 Dart 代码性能。 -
基准测试 (Benchmarking):你可以通过编写基准测试来衡量和跟踪应用的性能。Flutter Driver 库提供了对基准测试的支持。使用此集成测试框架,你可以生成跟踪卡顿、下载大小、电池效率和启动时间的指标。有关更多信息,请查看 集成测试。
-
Widget 重建分析器 (IntelliJ for Android Studio):卡顿通常源于不必要的 UI 重建。如果你使用的是 IntelliJ 或 Android Studio,Widget 重建分析器可以通过显示当前屏幕和帧的 widget 重建次数,帮助定位并修复这些问题。有关更多信息,请参阅 显示性能数据。
Flutter 旨在提供每秒 60 帧 (fps) 的性能,在支持的设备上可达 120 fps。为了达到 60fps,每一帧必须在大约 16ms 内完成渲染,以避免卡顿。当渲染一帧的时间过长导致该帧被丢弃时,就会发生卡顿,从而在动画中出现明显的停顿。例如,如果某一帧偶尔需要比平时长 10 倍的时间来渲染,它很可能会被丢弃,导致动画看起来不连贯。
连接到物理设备
#几乎所有的 Flutter 应用性能调试都应该在物理 Android 或 iOS 设备上进行,并将 Flutter 应用运行在 profile 模式下。使用 debug 模式,或在模拟器上运行应用,通常无法代表发布模式构建的最终表现。你应该考虑在用户可能使用的最慢的设备上检查性能。
在性能分析模式下运行
#Flutter 的 profile 模式编译和启动应用的方式几乎与 release 模式完全相同,但仅包含足够的额外功能以允许调试性能问题。例如,profile 模式为性能分析工具提供了追踪信息。
按如下方式在 profile 模式下启动应用:
-
在 VS Code 中,打开
launch.json文件,将flutterMode属性设置为profile(分析完成后,改回release或debug)。json"configurations": [ { "name": "Flutter", "request": "launch", "type": "dart", "flutterMode": "profile" } ] -
在 Android Studio 和 IntelliJ 中,使用 Run > Flutter Run main.dart in Profile Mode 菜单项。
-
在命令行中,使用
--profile标志。flutter run --profile
有关不同模式的更多信息,请参阅 Flutter 的构建模式。
你将从打开 DevTools 并查看性能图层开始,详见下一节。
启动 DevTools
#DevTools 提供了性能分析、堆内存检查、显示代码覆盖率、启用性能图层以及分步调试器等功能。DevTools 的时间轴视图 (Timeline view) 允许你逐帧调查应用的 UI 性能。
一旦应用在 profile 模式下运行,请启动 DevTools。
显示性能图层
#你可以按如下方式切换性能图层的显示:
-
DevTools 性能视图:启用 PerformanceOverlay widget 的最简单方法是通过 DevTools 中的 性能视图。只需点击 Performance Overlay 按钮即可切换正在运行的应用上的图层显示。
-
命令行:在命令行中使用 P 键切换性能图层。
-
以编程方式:要以编程方式启用图层,请参阅 以编程方式调试 Flutter 应用 页面中的 性能图层 一节。
观察性能图层
#性能图层在两个图表中显示统计信息,展示应用的时间消耗情况。如果 UI 出现卡顿(掉帧),这些图表可以帮助你找出原因。图表显示在运行应用的正上方,但它们不是作为普通 widget 绘制的——Flutter 引擎本身会绘制该图层,对性能的影响极小。每个图表代表该线程最近的 300 帧。
本节介绍如何启用性能图层并利用它诊断应用中卡顿的原因。以下屏幕截图显示了在 Flutter Gallery 示例上运行的性能图层:
性能图层显示光栅线程 (raster thread,上方) 和 UI 线程 (下方)。
垂直的绿色条代表当前帧。
查看图表
#上方图表(标记为 "GPU")显示了光栅线程消耗的时间,下方图表显示了 UI 线程消耗的时间。图表中的白色横线表示纵轴上 16ms 的增量;如果图表超过了这些线,说明你的运行速度低于 60Hz。横轴代表帧。图表仅在应用重绘时更新,因此如果应用处于空闲状态,图表将停止移动。
图层应始终在 profile 模式下查看,因为 debug 模式的性能为了支持有助于开发的昂贵断言而被故意牺牲,因此结果具有误导性。
每一帧都应在 1/60 秒(约 16ms)内完成创建和显示。超过此限制的帧(在任一图表中)将无法显示,导致卡顿,并在一个或两个图表中出现垂直的红色条。如果 UI 图表中出现红条,说明 Dart 代码开销过大。如果 GPU 图表中出现垂直红条,说明场景过于复杂,无法快速渲染。
垂直的红色条表示当前帧的渲染和绘制开销过大。
当两个图表都显示红色时,请从诊断 UI 线程开始。
查看线程
#Flutter 使用多个线程来执行其工作,尽管图层中仅显示了两个线程。你所有的 Dart 代码都运行在 UI 线程上。虽然你无法直接访问任何其他线程,但在 UI 线程上的操作会对其他线程产生性能影响。
- 平台线程 (Platform thread)
-
平台的的主线程。插件代码在此运行。有关详细信息,请参阅 iOS 的 UIKit 文档或 Android 的 MainThread 文档。该线程未在性能图层中显示。
- UI 线程 (UI thread)
-
UI 线程在 Dart VM 中执行 Dart 代码。该线程包含你编写的代码,以及 Flutter 框架代表你的应用执行的代码。当你的应用创建并显示一个场景时,UI 线程会创建一个 图层树 (layer tree)——一个包含设备无关绘制指令的轻量级对象,并将该图层树发送给光栅线程以在设备上渲染。不要阻塞此线程! 在性能图层的底部行显示。
- 光栅线程 (Raster thread)
-
光栅线程接收图层树并通过与 GPU(图形处理单元)通信来显示它。你无法直接访问光栅线程或其数据,但如果该线程运行缓慢,通常是因为你在 Dart 代码中所做的某些操作导致的。图形库 Skia 和 Impeller 运行在此线程上。在性能图层的顶部行显示。请注意,虽然光栅线程为 GPU 进行光栅化,但线程本身运行在 CPU 上。
- I/O 线程 (I/O thread)
-
执行昂贵的任务(主要是 I/O),这些任务否则会阻塞 UI 或光栅线程。该线程未在性能图层中显示。
有关更多信息和视频链接,请参阅 Flutter Wiki 中的 框架架构 (The Framework architecture),以及社区文章 分层蛋糕 (The Layer Cake)。
识别问题
#查看 UI 图表
#如果性能图层在 UI 图表中显示红色,请从分析 Dart VM 开始,即使 GPU 图表也显示红色。
查看 GPU 图表
#有时,一个场景产生的图层树虽然容易构建,但在光栅线程上渲染却非常耗时。出现这种情况时,UI 图表没有红色,但 GPU 图表显示红色。在这种情况下,你需要查明代码中是什么导致渲染过程变慢。某些特定的工作负载对 GPU 来说更困难。它们可能涉及对 saveLayer 的不必要调用、多个对象的不透明度重叠,以及在特定情况下的裁剪或阴影。
如果你怀疑缓慢的来源是动画,请点击 Flutter 检查器 (Inspector) 中的 Slow Animations 按钮,将动画速度降低 5 倍。如果你想更精确地控制速度,也可以以编程方式执行此操作。
缓慢发生在第一帧,还是整个动画过程中?如果是整个动画,是否是裁剪导致了变慢?也许有替代的绘图方法不需要裁剪。例如,在正方形上覆盖不透明的角,而不是裁剪成圆角矩形。如果是一个正在淡出、旋转或以其他方式操作的静态场景,RepaintBoundary 可能会有帮助。
检查屏幕外图层 (offscreen layers)
#saveLayer 方法是 Flutter 框架中最昂贵的方法之一。它在对场景进行后期处理时很有用,但它会拖慢应用速度,如果不需要,应避免使用。即使你没有显式调用 saveLayer,系统也可能代表你进行隐式调用,例如在指定 Clip.antiAliasWithSaveLayer(通常作为 clipBehavior)时。
例如,假设你有一组使用 saveLayer 渲染的不透明对象。在这种情况下,将不透明度分别应用于每个单独的 widget,而不是应用于 widget 树中更上层的父 widget,通常性能会更好。其他潜在的昂贵操作(如裁剪或阴影)也是如此。
当你遇到对 saveLayer 的调用时,问问自己以下问题:
- 应用需要这个效果吗?
- 这些调用中有任何可以消除的吗?
- 我可以将同样的效果应用到单个元素而不是一组元素上吗?
检查非缓存图像
#使用 RepaintBoundary 缓存图像是好的,前提是它有意义。
从资源角度来看,最昂贵的操作之一是渲染包含图像文件的纹理。首先,压缩后的图像从持久存储中获取。然后解压缩到主机内存(GPU 内存)中,最后传输到设备内存 (RAM) 中。
换句话说,图像 I/O 可能很昂贵。缓存提供了复杂层级的快照,以便在后续帧中更容易渲染。因为光栅缓存条目构建成本高昂且占用大量 GPU 内存,所以仅在绝对必要时才缓存图像。
其他资源
#以下资源提供了有关使用 Flutter 工具和在 Flutter 中进行调试的更多信息:
- 调试
- 性能视图
- Flutter 检查器
- Flutter 检查器讲座,于 DartConf 2018 发表
- 为什么 Flutter 使用 Dart,Hackernoon 上的文章
- 为什么 Flutter 使用 Dart,Flutter 频道上的视频
- DevTools:Dart 和 Flutter 应用的性能工具
-
Flutter API 文档,特别是
PerformanceOverlay类和dart:developer包