总的来说,Flutter 应用默认情况下是高性能的,因此您只需要避免常见的陷阱就能获得出色的性能。这些最佳实践建议将帮助您编写出尽可能高性能的 Flutter 应用。

您如何设计一个 Flutter 应用以最高效地渲染您的场景?特别是,您如何确保框架生成的绘制代码尽可能高效?一些渲染和布局操作已知速度较慢,但并非总是可以避免。应谨慎使用它们,并遵循以下指南。

最小化昂贵操作

#

某些操作比其他操作更昂贵,这意味着它们消耗更多的资源。显然,您只希望在必要时使用这些操作。您的应用 UI 的设计和实现方式会对运行效率产生很大影响。

控制 build() 的成本

#

在设计 UI 时,请牢记以下几点:

  • 避免在 build() 方法中进行重复且耗时的操作,因为当父级小部件重建时,build() 可能会被频繁调用。
  • 避免创建具有大型 build() 函数的单个过大的 widget。根据封装性以及它们的变化方式,将它们拆分成不同的 widget。
    • 当在 State 对象上调用 setState() 时,所有子孙 widget 都会重建。因此,将 setState() 调用限制在 UI 实际需要更改的子树部分。如果更改仅限于子树的一小部分,请避免在高层级调用 setState()
    • 当重新遇到与上一帧相同的子 widget 实例时,重建所有子孙的遍历会停止。此技术在框架内大量用于优化动画,当动画不影响子孙子树时。请参阅 TransitionBuilder 模式和 SlideTransition 的源代码,它利用此原理在动画时避免重建其子孙。(“相同的实例”通过 operator == 进行评估,但关于何时避免重写 operator == 的建议,请参阅页面末尾的陷阱部分。)
    • 尽可能多地在 widget 上使用 const 构造函数,因为它们允许 Flutter 缩短大部分重建工作。要自动获得使用 const 的提醒,请启用 flutter_lints 包中推荐的 lint。有关更多信息,请查看 flutter_lints 迁移指南
    • 要创建可重用的 UI 片段,请优先使用 StatelessWidget 而不是函数。

有关更多信息,请参阅:


使用 StringBuffer 来高效地构建字符串

#

当需要将多个部分合并成一个字符串时,尤其是在循环内部,使用 + 运算符可能会效率低下,因为它会在每次连接时创建一个新的 String 对象。更好的方法是使用 StringBuffer,它会收集所有字符串,并在调用 toString() 时只连接一次。

在 YouTube 上以新标签页观看:“StringBuffer (Technique of the Week)”


谨慎使用 saveLayer()

#

一些 Flutter 代码使用 saveLayer()(一种昂贵的操作)来实现 UI 中的各种视觉效果。即使您的代码没有显式调用 saveLayer(),您使用的其他 widget 或包也可能在后台调用它。也许您的应用调用 saveLayer() 的次数超过了必要,过多的 saveLayer() 调用会导致卡顿。

为什么 saveLayer() 昂贵?

#

调用 saveLayer() 会分配一个屏幕外缓冲区,并将内容绘制到屏幕外缓冲区可能会触发渲染目标切换。GPU 就像一个消防水龙带,而渲染目标切换会迫使 GPU 暂时重定向该流,然后再将其导回。在移动 GPU 上,这对渲染吞吐量尤其具有破坏性。

什么时候需要 saveLayer()?

#

在运行时,如果您需要动态显示来自服务器(例如)的各种形状,每个形状都有一些透明度,这些形状可能会(也可能不会)重叠,那么您几乎必须使用 saveLayer()

调试 saveLayer() 调用

#

您如何知道您的应用直接或间接调用 saveLayer() 的频率?saveLayer() 方法会在 DevTools 时间轴上触发一个事件;通过检查 DevTools Performance 视图中的 PerformanceOverlayLayer.checkerboardOffscreenLayers 开关,了解您的场景何时使用 saveLayer

最小化 saveLayer() 调用

#

您能否避免调用 saveLayer?这可能需要重新考虑您创建视觉效果的方式。

  • 如果调用来自您的代码,您能否减少或消除它们?例如,您的 UI 可能重叠了两个形状,每个形状都有非零透明度。

    • 如果它们总是以相同的方式、相同的重叠量、相同的透明度重叠,您可以预先计算出这个重叠的半透明对象的外观,将其缓存起来,然后使用它而不是调用 saveLayer()。这适用于任何您可以预先计算的静态形状。
    • 能否重构您的绘制逻辑以完全避免重叠?
  • 如果调用来自您不拥有的包,请联系包所有者并询问为什么这些调用是必要的。能否减少或消除它们?如果不能,您可能需要寻找另一个包,或编写自己的包。

其他可能触发 saveLayer() 且可能昂贵的 widget:

  • ShaderMask
  • ColorFilter
  • Chip—如果 disabledColorAlpha != 0xff,可能会触发对 saveLayer() 的调用。
  • Text—如果存在 overflowShader,可能会触发对 saveLayer() 的调用。

最小化 opacity 和 clipping 的使用

#

Opacity 和 clipping 也是昂贵的操作。以下是一些您可能会觉得有用的技巧:

  • 仅在必要时使用 Opacity widget。请参阅 Opacity API 页面上的 透明图像 部分,其中有一个直接将不透明度应用于图像的示例,这比使用 Opacity widget 更快。
  • 与其将简单的形状或文本包装在 Opacity widget 中,不如通常使用半透明颜色绘制它们更快。(尽管这只适用于要绘制的形状中没有重叠部分的情况。)
  • 要实现图像渐隐效果,请考虑使用 FadeInImage widget,它使用 GPU 的片段着色器应用渐进式不透明度。有关更多信息,请查看 Opacity 文档。
  • Clipping 不会调用 saveLayer()(除非明确使用 Clip.antiAliasWithSaveLayer 请求),因此这些操作不像 Opacity 那样昂贵,但 clipping 仍然很昂贵,所以请谨慎使用。默认情况下,clipping 被禁用(Clip.none),因此您必须在需要时显式启用它。
  • 要创建带有圆角的矩形,请不要应用 clipping 矩形,而是考虑使用许多 widget 类提供的 borderRadius 属性。

谨慎实现 grids 和 lists

#

您的 grids 和 lists 的实现方式可能会导致应用的性能问题。本节介绍创建 grids 和 lists 时的一项重要最佳实践,以及如何确定您的应用是否使用了过多的布局传递。

保持惰性!

#

构建大型 grid 或 list 时,请使用惰性构建器方法和回调。这确保在启动时只构建屏幕上可见的部分。

有关更多信息和示例,请参阅:

避免 intrinsic 操作

#

有关 intrinsic 传递可能导致 grids 和 lists 出现问题的详细信息,请参阅下一节。


最小化由 intrinsic 操作引起的布局传递

#

如果您已经进行了大量的 Flutter 编程,您可能熟悉 创建 UI 时布局和约束的工作原理。您甚至可能已经记住了 Flutter 的基本布局规则:约束向下传递,尺寸向上返回,父级设置位置。

对于某些 widget,尤其是 grids 和 lists,布局过程可能会很昂贵。Flutter 努力只对 widget 进行一次布局传递,但有时需要第二次传递(称为intrinsic传递),这会降低性能。

什么是 intrinsic 传递?

#

当您希望所有单元格具有最大或最小单元格的尺寸(或类似需要轮询所有单元格的计算)时,就会发生 intrinsic 传递。

例如,考虑一个包含大量 Card 的 grid。一个 grid 应该具有统一大小的单元格,因此布局代码会执行一次传递,从 grid 的根(在 widget 树中)开始,询问 grid 中的每个卡片(不只是可见的卡片)返回其intrinsic大小——即 widget 在没有约束条件下的首选大小。有了这些信息,框架会确定一个统一的单元格大小,并第二次遍历所有 grid 单元格,告知每个卡片要使用的大小。

调试 intrinsic 传递

#

要确定您是否有过多的 intrinsic 传递,请在 DevTools 中启用Track layouts 选项(默认禁用),并查看应用的 堆栈跟踪,了解执行了多少次布局传递。启用跟踪后,intrinsic 时间轴事件会标记为 '$runtimeType intrinsics'。

避免 intrinsic 传递

#

您有几种选择可以避免 intrinsic 传递:

  • 预先将单元格设置为固定大小。
  • 选择一个特定的单元格作为“锚点”单元格——所有单元格的大小都将相对于此单元格。编写一个自定义 RenderObject,它首先定位子锚点,然后围绕它布局其他子项。

要更深入地了解布局的工作原理,请参阅 Flutter 架构概述中的 布局和渲染部分。


在 16 毫秒内构建和显示帧

#

由于构建和渲染有两个独立的线程,因此在 60Hz 显示器上,您有 16 毫秒用于构建,16 毫秒用于渲染。如果延迟是一个问题,请在 16 毫秒或更短的时间内构建和显示一帧。请注意,这意味着在 8 毫秒或更短时间内构建,在 8 毫秒或更短时间内渲染,总共不超过 16 毫秒。

如果在profile 模式下,您的帧渲染时间远低于 16 毫秒,即使存在一些性能陷阱,您可能也不必担心性能,但您仍应努力尽快构建和渲染帧。原因如下:

  • 将帧渲染时间降低到 16 毫秒以下可能不会产生视觉上的差异,但它会提高电池续航能力并减少发热问题。
  • 它可能在您的设备上运行良好,但请考虑您要针对的最低端设备的性能。
  • 随着 120fps 设备越来越普及,您将希望在 8 毫秒(总计)内渲染帧,以提供最流畅的体验。

如果您想知道为什么 60fps 能带来流畅的视觉体验,请观看视频为什么是 60fps?

陷阱

#

如果您需要调整应用的性能,或者 UI 不如您预期的流畅,DevTools Performance 视图可以提供帮助!

此外,您 IDE 的 Flutter 插件也可能很有用。在 Flutter Performance 窗口中,勾选Show widget rebuild information复选框。此功能可帮助您检测何时渲染和显示帧的时间超过 16 毫秒。在可能的情况下,插件会提供指向相关提示的链接。

以下行为可能会对您的应用性能产生负面影响。

  • 避免使用 Opacity widget,尤其是在动画中避免使用。请改用 AnimatedOpacityFadeInImage。有关更多信息,请参阅 Opacity 动画的性能注意事项

  • 在使用 AnimatedBuilder 时,避免在 builder 函数中放置一个会构建与动画无关的 widget 的子树。这个子树会为动画的每一次 tick 而重建。相反,一次性构建该子树的一部分,并将其作为子项传递给 AnimatedBuilder。有关更多信息,请参阅 性能优化

  • 在动画中避免 clipping。如果可能,请在动画化图像之前对其进行预裁剪。

  • 如果大多数子项不在屏幕上可见,请避免使用具有具体 List 子项的构造函数(例如 Column()ListView()),以避免构建成本。

  • 避免重写 Widget 对象上的 operator ==。虽然它看起来可以避免不必要的重建,但实际上它会损害性能,因为它会导致 O(N²) 的行为。此规则的唯一例外是叶子 widget(没有子项的 widget),在特定情况下,比较 widget 的属性可能比重建 widget 更有效,并且 widget 的配置很少更改。即使在这种情况下,通常也最好依赖于缓存 widget,因为即使一次对 operator == 的重写也可能导致全局性能下降,因为编译器无法再假定调用始终是静态的。

资源

#

有关更多性能信息,请参阅以下资源: