Flutter 性能分析

你将学到

  • Flutter 的目标是提供 60 帧每秒(fps)的性能,或者是在可以达到 120 Hz 的设备上提供 120 fps 的性能。

  • 对于 60 fps 来说,需要在约每 16 ms 的时候渲染一帧。

  • 当 UI 渲染不流畅的时候,卡顿就随之产生了。举例来说,如果一帧花了 10 倍的时间来渲染,这帧就会被丢弃,动画看起来就会卡。

有句话叫“的应用固然很好,但流畅的应用则更好。”如果你的应用渲染并不流畅,该怎么处理呢?从哪里着手呢?本文展示了应该从哪里着手,步骤以及可以提供帮助的工具。

备忘

  • 应用的性能不只是由一次测量 (measure) 决定的。性能有时取决于原生速度,同时也取决于 UI 的流畅性,不卡顿。其他性能指标还包括 I/O 或者网速。本文主要聚焦于第二种性能(UI 流畅性),但其中的大多数工具也能被用来分析其他性能问题。

  • 分析 Dart 代码中的性能问题,可以参考 调试 Flutter 应用 页下的 跟踪 Dart 代码性能

分析性能问题

分析应用的性能问题需要打开性能监控图层 (performance overlay) 来观察 UI 和 GPU 线程。在此之前,要确保是在 分析模式 下运行,而且当前设备不是虚拟机。使用用户可能采用的最慢设备来获取最佳结果。

连接到物理设备

几乎全部的 Flutter 应用性能调试都应该在真实的 Android 或者iOS 设备上以 分析模式 进行。通常来说,调试模式或者是模拟器上运行的应用的性能指标和发布模式的表现并不相同。应该考虑在用户使用的最慢的设备上检查性能。

为什么应该在真机上运行:

  • 各种模拟器使用的硬件并不相同,因此性能也不同—模拟器上的一些操作会比真机快,而另一些操作则会比真机慢。

  • 调试模式相比分析模式或者发布编译来说,增加了额外的检查(例如断言),这些检查可能相当耗费资源。

  • 调试模式和发布模式代码执行的方式也是不同的。调试编译采用的是“just in time” (JIT) 模式运行应用,而分析和发布模式则是预编译到本地指令(“ahead of time”,或者叫 AOT)之后再加载到设备中。JIT本身的编译就可能导致应用暂停,从而导致卡顿。

在分析模式运行

除了一些调试性能问题所必须的额外方法,Flutter 的分析模式和发布模式的编译和运行基本相同。例如,分析模式为分析工具提供了追踪信息。

使用分析模式运行应用的方法:

  • 在 Android Studio 和 IntelliJ 使用Run > Flutter Run main.dart in Profile Mode 选项

  • 在 VS Code 中,打开 launch.json 文件,设置 flutterMode 属性为 profile(当分析完成后,改回 release 或者 debug):

  1. "configurations": [
  2. {
  3. "name": "Flutter",
  4. "request": "launch",
  5. "type": "dart",
  6. "flutterMode": "profile"
  7. }
  8. ]
  • 命令行使用 —profile 参数运行
  1. $ flutter run --profile

关于不同模式的更多信息,请参考 Flutter 的构建模式选择

下面我们会从打开 DevTools、查看性能图层开始讲述。

运行 DevTools

Dart DevTool 提供诸如性能分析、堆测试以及显示代码覆盖率等功能。DevTool 的 [Timeline] 界面可以让开发者逐帧分析应用的 UI 性能。

一旦你的应用程序在分析模式下运行,即 运行了 DevTools

性能图层

性能图层用两张图表显示应用的耗时信息。如果 UI 产生了卡顿(跳帧),这些图表可以帮助分析原因。图表在当前应用的最上层展示,但并不是用普通的 widget 方式绘制的—Flutter 引擎自身绘制了该图层来尽可能减少对性能的影响。每一张图表都代表当前线程的最近 300 帧表现。

本节阐述如何打开性能图层并用其来分析应用中卡顿的原因。下面的截图展示了 Flutter Gallery 样例的性能图层:

Screenshot of overlay showing zero jank

GPU 线程的性能情况在上面,UI 线程显示在下面。垂直的绿色条条代表的是当前帧。

图表解释

最顶部的图形表示 GPU 线程所花费的时间,底部的图表显示了 UI 线程所花费的时间。横跨图表中的白线代表了 16 ms 内沿竖轴的增量;如果这些线在图表中都没有超过它的话,说明你的运行帧率低于 60 Hz。而横轴则表示帧。只有当你的应用绘制时这个图表才会更新,所以如果它空闲的话,图表就不会动。

这个浮层只应在 分析模式 中使用,因为在 调试模式 下有意牺牲了性能来换取昂贵的断言以帮助开发,所以这时候的结果会有误导性。

每一帧都应该在 1/60 秒(大约 16 ms)内创建并显示。如果有一帧超时(任意图像)而无法显示,就导致了卡顿,图表之一就会展示出来一个红色竖条。如果是在 UI 图表出现了红色竖条,则表明 Dart 代码消耗了大量资源。而如果红色竖条是在 GPU 图表出现的,意味着场景太复杂导致无法快速渲染。

Screenshot of performance overlay showing jank with red bars

红色竖条表明当前帧的渲染和绘制都很耗时。当两张图表都是红色时,就要开始对 UI 线程 (Dart VM) 进行诊断了。

Flutter 的线程

Flutter 用了一些额外的线程来完成这项工作。开发者的 Dart 代码都在 UI 线程运行。尽管没有直接访问其他线程的权限,但 UI 线程的动作还是对其他线程的性能有影响的。

平台线程

该平台的主线程。插件代码在这里运行。更多信息请参阅:iOS 的程序 (UIKit) 文档,或者 Android 的主线程 (MainThread) 文档。性能图层并不会展示该线程。

UI 线程

UI 线程在 Dart VM 执行 Dart 代码。该线程包括开发者写下的代码和 Flutter 框架根据应用行为生成的代码。当应用创建和展示场景的时候,UI 线程首先建立一个 图层树(layer tree) ,一个包含设备无关的渲染命令的轻量对象,并将图层树发送到 GPU 线程来渲染到设备上。不要阻塞这个线程!在性能图层的最低栏展示该线程。

GPU 线程

GPU 线程取回图层树并通知 GPU 渲染。尽管无法直接与 GPU 线程或其数据通信,但如果该线程变慢,一定是开发者 Dart 代码中的某处导致的。图形库 Skia 在该线程运行,有时也被叫做光栅器 (rasterizer) 线程。在性能图层的最顶栏显示该线程。

I/O 线程

可能阻塞 UI 或者 GPU 线程的耗时任务(大多数情况下是 I/O)。该线程并不会在性能图层中展示。

你可以在 GitHub wiki 上的框架结构 (The Framework architecture) 一文中了解更多信息和一些视频内容,另外你可以在我们的社区中查看文章 The Layer Cake

显示性能图层

你可以用如下方法显示性能图层:

  • 使用 Flutter Inspector

  • 从命令行启动

  • 写入代码

使用 Flutter inspector

打开 PerformanceOverlay widget 最简单的方法是 IDE 中 Flutter 插件提供的 Flutter inspector,你可以在 开发者工具使用 Flutter inspector 工具 中找到。只需单击 Performance Overlay 按钮,即可在正在运行的应用程序上切换图层。

命令行

使用 p 参数触发性能图层。

代码控制

要用代码实现性能图层,可以查看添加输出代码的方式调试 Flutter 应用 中的 Performance overlay 章节。

可能读者已经对 Flutter Gallery 样例应用相当熟悉了。要在 Flutter Gallery 中使用性能图层,请使用与 Flutter 一起安装的 examples 目录的副本在分析模式下运行应用。应用的代码中已经写好了通过应用菜单动态触发图层,同时允许对 saveLayer 的调用和当前已缓存的图片的检查。

备忘

从应用市场下载的 Flutter Gallery 应用是无法打开性能图层的。因为该版本是用发布模式编译的(而不是分析模式),并且没有提供图层开关的菜单。

定位 UI 图表中的问题

如果性能图层的 UI 图表显示红色,就要从分析 Dart VM 开始着手了,即使 GPU 图表同样显示红色。

思考:除了说“使用DevTools调试”,这里还可以说什么?

定位 GPU 图表中的问题

有些情况下界面的图层树构造起来虽然容易,但在 GPU 线程下渲染却很耗时。这种情况发生时,UI 图表没有红色,但 GPU 图表会显示红色。这时需要找出代码中导致渲染缓慢的原因。特定类型的负载对 GPU 来说会更加复杂。可能包括不必要的对 saveLayer 的调用,许多对象间的复杂操作,还可能是特定情形下的裁剪或者阴影。

如果推断的原因是动画中的卡顿的话,可以点击 Flutter inspector 中的 Slow Animations 按钮,来使动画速度减慢 5x。如果你想从更多方面控制动画速度,你可以参考 programmatically

卡顿是第一帧发生的还是贯穿整个动画过程呢?如果是整个动画过程的话,会是裁剪导致的吗?也许有可以替代裁剪的方法来绘制场景。比如说,不透明图层的长方形中用尖角来取代圆角裁剪。如果是一个静态场景的淡入、旋转或者其他操作,可以尝试使用重绘边界 (RepaintBoundary)。

检查屏幕之外的视图

保存图层 (saveLayer) 方法是 Flutter 框架中最重量的操作之一。更新屏幕时这个方法很有用,但它可能使应用变慢,如果不是必须的话,应该避免使用这个方法。即便没有显式地调用 saveLayer,也可能在其他操作中间接调用了该方法。可以使用棋盘画面以外的层(PerformanceOverlayLayer.checkerboardOffscreenLayers) 开关来检查场景是否使用了 saveLayer

打开开关之后,运行应用并检查是否有图像的轮廓闪烁。如果有新的帧渲染的话,容器就会闪烁。举个例子,也许有一组对象的透明度要使用 saveLayer 来渲染。在这种情况下,相比通过 widget 树中高层次的父 widget 操作,单独对每个 widget 来应用透明度可能性能会更好。其他可能大量消耗资源的操作也同理,比如裁剪或者阴影。

备忘

透明度、裁剪以及阴影它们本身并不是个糟糕的注意。然而对 widget 树顶层 widget 的操作可能导致额外对 saveLayer 的调用以及无用的处理。

当遇到对 saveLayer 的调用时,先问问自己:

  • 应用是否需要这个效果?

  • 可以减少调用么?

  • 可以对单独元素操作而不是一组元素么?

检查没有缓存的图像

使用重绘边界 (RepaintBoundary) 来缓存图片是个好主意,当需要的时候。

从资源的角度看,最重量级的操作之一是用图像文件来渲染纹理。首先,需要从持久存储中取出压缩图像,然后解压缩到宿主存储中(GPU 存储),再传输到设备存储器中 (RAM) 。

也就是说,图像的 I/O 操作是重量级的。缓存提供了复杂层次的快照,这样就可以方便地渲染到随后的帧中。因为光栅缓存入口的构建需要大量资源,同时增加了 GPU 存储的负载,所以只在必须时才缓存图片。

打开覆盖层性能棋盘格光栅缓存图像(PerformanceOverlayLayer.checkerboardRasterCacheImages) 开关可以检查哪些图片被缓存了。

运行应用来查看使用随机颜色网格渲染的图像,标识被缓存的图像。当和场景交互时,网格里的图片应该是静止的—代表重新缓存图片的闪烁视图不应该出现。

大多数情况下,开发者都希望在网格里看到的是静态图片,而不是非静态图片。如果静态图片没有被缓存,可以将其放到重绘边界(RepaintBoundary) widget 中来缓存。虽然引擎也可能忽略 repaint boundary,如果它认为图像还不够复杂的话。

检视 widget 重建性能

Flutter 框架的设计使得构建达不到 60 fps 流畅度的应用变得困难。通常情况下如果卡顿,就是因为每一帧被重建的 UI 比需求更多的简单 bug。Widget rebuild profiler 可以帮助调试和修复这些问题引起的 bug。

可以检视 widget inspector 中当前屏幕和帧下的 widget 重建数量。了解细节,可以参考在 Android Studio 或类 IntelliJ 里开发 Flutter 应用 中的 显示性能数据

评分

可以通过编写评分测试来测量和追踪应用的性能。Flutter Driver 库提供了对评分的支持。基于这套测试框架就可以生成以下几项的测试标准:

  • 卡顿

  • 下载大小

  • 电池性能

  • 启动时间

追踪这些评分可以在回归测试中了解对性能的不利影响。

了解更多,请参考 测试 Flutter 应用 中的 集成测试 一节。

更多资源

以下链接提供了关于 Flutter 工具的使用和 Flutter 调试的更多信息: