Flutter 应用性能优化最佳实践

通常来说,Flutter 构建的应用程序在默认情况下都是高性能的。所以你只需要避开常见的陷阱,不需要使用复杂的分析工具对细节做优化,就可以获得优异的性能,这些最佳建议将帮助你编写性能最佳的 Flutter 应用程序。

如何设计一个能最有效地渲染页面的 Flutter 应用程序?特别是如何确保底层框架生成的绘图代码尽可能高效?你需要经过谨慎地思考之后再使用那些已知性能很低的渲染和布局操作。请遵循下面的指导。

尽量减少消耗资源的操作

有些操作特别消耗资源。很显然,你只想在必要地时候使用这些操作。在这种情况下,如何设计和实现应用的 UI,对运行效率有很大影响。

控制 build() 方法的耗时

以下是在设计你的 UI 时的一些建议:

  • 避免在 build() 方法中进行重复且耗时的工作,因为当父 widget 重建时,子 Wdiget 的 build() 方法会被频繁地调用。

  • 避免在一个超长的 build() 方法中返回一个过于庞大的 widget。把它们分拆成不同的 widget,并进行封装,另外它们要这样改变:

    • 当在 State 对象上调用 setState()时,所有后代 widget 都将重建。因此,将 setState() 的调用转移到其 UI 实际需要更改的 widget 子树部分。如果改变的部分仅包含在 widget 树的一小部分中,请避免在 widget 树的更高层级中调用 setState()

    • 当重新遇到与前一帧相同的子 widget 实例时,将停止遍历。这种技术在框架内部大量使用,用于优化动画不影响子树的动画。请参阅 TransitionBuilder 模式和遵循此原则的 SlideTransition 代码,以避免在动画过程中重建其后代 widget。(“相同的实例”是使用 operator == 进行评估的,但是请参阅本页面末尾的陷阱部分,了解有关何时避免覆盖 operator == 的建议。)

    • 请尽可能地在 widget 上使用 const 构造函数,这将让 Flutter 的 widget 重建时间大幅缩短。要自动提醒使用 const,请启用 flutter_lints package 中推荐的 lints。了解更多信息,请查看 flutter_lints 迁移指南.

    • 在构建可复用的 UI 代码时,最好使用 StatelessWidget 而不是函数。

了解更多信息,请查看:


谨慎使用 saveLayer()

一些 Flutter 代码调用了性能代价很大的 saveLayer() 方法来实现 UI 中的各种视觉效果。你使用的其他 widget 或者 package 可能会在幕后调用它。也许你的应用程序会大量调用 saveLayer();过多调用 saveLayer() 会导致卡顿。

为什么 saveLayer 代价很大?

调用 saveLayer() 会开辟一片离屏缓冲区并将内容绘制到离屏缓冲区可能会触发渲染目标切换。 GPU 希望直达目标地运行,但渲染目标迫使 GPU 暂时重定向到该数据流,然后又直接把它切回来。这样会对渲染吞吐量造成特别大的破坏性。

什么时候需要 saveLayer?

在运行时,如果你需要动态地显示各种形状效果(例如),每个形状都有一定地透明度,可能(或可能不)重叠,那么你几乎必须使用 saveLayer()

调试 saveLayer 的调用

你如何才能知道应用程序调用 saveLayer() 的频率,直接还是间接调用? saveLayer() 方法会触发 DevTools timeline 上的一个事件;通过检查 DevTools 性能视图 (DevTools Performance view) 中的 PerformanceOverlayLayer.checkerboardOffscreenLayers 来了解何时在使用 saveLayer

尽量减少 saveLayer 的调用

你能避免对 saveLayer 的调用吗?这可能需要你重新思考如何创建视觉效果:

  • 如果这些调用是来自于 的代码,你能减少或消除它们吗?例如,也许你的用户界面重叠了两个形状,每个形状都有非零的透明度:

    • 如果它们总是以相同的数量、相同的方式、相同的透明度进行重叠,你就可以预先计算出这个重叠的、半透明物体的样子,将其缓存起来,然后用它代替调用 saveLayer()。这适用于任何你可以预先计算的静态形状。

    • 你可以换一种绘画形式以完全避免重叠吗?

  • 如果这些调用不是来自你的 package,请联系 package 的所有者并询问为什么需要使用这些调用。能否减少或消除它们?如果不能,你可能需要寻找其他作者的 package,或自己编写。

其他会触发 saveLayer() 的 widget,也有潜在的性能代价:

  • ShaderMask
  • ColorFilter
  • Chip— 当 disabledColorAlpha != 0xff 的时候,会调用 saveLayer()

  • Text— 当有 overflowShader 时,会调用saveLayer()


尽量减少使用不透明度和裁剪

不透明度和裁剪都是特别消耗资源的操作。这里有一些建议也许对你有帮助:

  • 能不用 Opacity widget,就尽量不要用。有关将透明度直接应用于图像的示例,请查看 Transparent image,这比使用 Opacity widget 更快。

  • 与其将简单的形状或文本包裹在一个 Opacity widget 中,不如用半透明的颜色来绘制它们会更快。(这仅在要画的形状中没有重叠的部分时有效)。

  • 要在图像中实现淡入淡出,请考虑使用 FadeInImage widget,该 widget 使用 GPU 的片段着色器应用渐变不透明度。了解更多详情,请查看 Opacity 文档。

  • Clipping 不会调用 saveLayer() (除非明确使用 Clip.antiAliasWithSaveLayer),因此这些操作没有 Opacity 那么耗时,但仍然很耗时,所以请谨慎使用。

  • 要创建带圆角的矩形,而不是裁剪矩形来达到圆角的效果,请考虑使用很多 widget 都提供的 borderRadius 属性。


谨慎使用网格列表和列表

你的网格列表和列表的实现方式可能会给你的应用程序带来性能问题。本节介绍了创建网格列表和列表时的一个重要的最佳实现方式,以及如何确定你的应用程序是否使用了过多的布局传递。

懒加载!

当构建一个大型网格列表或列表时,使用带有回调的懒惰构建器方法。这可以确保在启动时只构建屏幕的可见部分。

了解更多的信息和示例,请查看:

避免内部传递

关于内部传递可能导致你的网格列表和列表出现问题的信息,见下一节。


尽量减少由内部操作引起的布局传递

如果你经常使用 Flutter 编程,你可能已经熟悉了在创建 UI 时 布局和约束是如何工作的。你甚至可能已经记住了 Flutter 的基本布局规则:
首先,上层 widget 向下层 widget 传递约束条件;
然后,下层 widget 向上层 widget 传递大小信息;
最后,上层 widget 决定下层 widget 的位置。

对于一些 widget,特别是网格列表和列表,布局过程中性能代价很大。 Flutter 尽可能在 widget 上只执行一次布局传递,但有时需要再次传递(称为 内部传递),这可能会降低性能。

什么是内部传递?

例如,你想要所有单元格都具有或大或小的效果(或类似需要轮询所有单元格的计算)时,就会发生内部传递。

例如,考虑一个大型的 卡片 网格列表时。一个网格列表应该有统一大小的单元格,所以布局代码执行了一次传递,从网格列表的根部开始(在 widget 树中),要求网格列表中的 每个 卡片(不仅仅是可见的卡片)来返回 内部 尺寸—假设没有任何限制,widget 更喜欢这样的尺寸。有了这些信息,底层框架就确定了一个统一的单元格尺寸,并再次重新访问所有的网格单元,告诉每个卡片应该使用什么尺寸。

调试内部传递

要确认你是否有过多的内部传递,请在 DevTools 中启用 布局跟踪 (Track layouts option) (默认情况下是禁用的),并查看应用程序的 堆栈跟踪 (stack trace) 以了解执行了多少次布局传递。一旦你启用跟踪,内部的时间线事件将被标记为 “$runtimeType intrinsics”。

避免内部传递

你有几个选择来避免内部传递:

  • 将单元格设置为固定大小。

  • 选择一个特定的单元格作为 “锚” —所有单元格的尺寸都将相对于这个单元格。编写一个自定义渲染对象,首先定位子锚点,然后在其周围布置其他子单元。

要更深入地了解布局的工作原理,请查看 Flutter 架构概览 中的 布局和渲染


在 16ms 内渲染完成每一帧

由于构建和渲染有两个独立的线程,因此构建时间为 16ms,60Hz 显示器上渲染时间为 16ms。如果需要考虑延迟,就要在 16ms 或更短 的时间内构建和显示帧。请注意,这意味着构建需要少于 8ms,渲染也需要少于 8ms,总计 16ms 或更短。

如果在 profile 构建 状态下,每一帧渲染时间低于 16ms,你可能不必担心性能问题以及一些性能陷阱,但仍然应该致力于尽可能快地渲染每一帧。为什么?

  • 将帧渲染时间降低到 16ms 以下可能在视觉上看不出来什么变化,但可以 延长电池寿命 以及避免发热问题。

  • 可能在你当前测试设备上运行良好,但请考虑在应用所支持的最低端设备上的情况。

  • 当 120fps 的设备普及之后,便需要在 8ms 之内完成每一帧的渲染来保证流畅平滑的体验。

如果你想弄明白为什么 60fps 会带来平滑的视觉体验,请看视频 60fps 是啥意思?

陷阱

如果你需要改善应用程序的性能,或者 UI 流畅度没达到你的预期,那么 DevTools 性能视图 (DevTools Performance view) 可以帮到你!

另外,IDE 中 Flutter plugin 也许也能帮到你。在 Flutter Performance 窗口中,勾选 Show widget rebuild information 复选框。此功能可帮助你检测帧的渲染和显示时间是否超过 16ms。插件也会尽可能提供指向相关提示的链接。

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

  • 避免使用 Opacity widget,尤其是在动画中避免使用。可以使用 AnimatedOpacityFadeInImage 代替该操作。更多信息,请参阅 Performance considerations for opacity animation

  • 使用 AnimatedBuilder 时,请避免在不依赖于动画的 widget 的构造方法中构建 widget 树,不然,动画的每次变动都会重建这个 widget 树,应当将这部分子树作为 child 传递给 AnimatedBuilder,从而只构建一次。更多内容,请参阅 Performance optimizations

  • 避免在动画中裁剪,尽可能的在动画开始之前预先裁剪图像。

  • Avoid using constructors with a concrete List of children (such as Column() or ListView()) if most of the children are not visible on screen to avoid the build cost.

  • 如果大多数 children widget 在屏幕上不可见,请避免使用返回具体列表的构造函数(例如 Column()ListView()),以避免构建成本。

  • 避免在 Widget 对象上重写 operator ==。虽然这看起来有助于避免不必要的重建,但在实践中,它实际上损害了性能,因为这是 O(N²) 的行为。只有 leaf widget(没有子的 widget)是个例外,在这种特殊的情况下,比较 widget 的属性可能比重建 widget 更加有效,也能更少改变 widget 的配置。即使在这种情况下,最好还要缓存 widget,因为哪怕有一次对 operator == 进行覆盖也会导致全面性能的下降,编译器也会因此不再认为调用总是静态的。

参考资料

了解更多性能信息,请参阅以下资源: