Flutter 性能优化最佳实践
在竞争激烈的移动领域,流畅且响应迅速的用户体验不仅是一种优势,它还是一种必要条件。然而,Flutter 开发者经常面临着创建高性能应用程序的挑战,与潜在的延迟、卡顿和响应缓慢作斗争。有一些最佳实践可以进一步提高性能。
使用最新版本的 Flutter
最新版本的 Flutter 始终包含最新的性能优化。因此,保持你的 Flutter SDK 为最新版本非常重要。
避免使用调试模式
Flutter 中的调试模式可用于调试代码,但它也会显著影响性能。因此,在生产环境中避免使用调试模式非常重要。
使用合适的 Widget
Flutter 提供了各种各样的组件,可用于构建用户界面。选择使用合适的组件非常重要,因为一些组件比其他组件性能更好。例如,ListView
的性能优于 GridView
。
避免不必要的重建
当对象的 state 更改时,Flutter 会自动重建使用该对象的组件。但是,如果组件不必要地重建,它可能会影响性能。因此,避免不必要的重建非常重要。
使用 profile 模式运行进行性能分析
Flutter 的 profile 模式几乎与 release 模式相同地编译并启动应用程序,但仅添加了足够的功能以允许调试性能问题。例如,分析模式会向分析工具提供跟踪信息。
注意:DevTools 无法连接到以分析模式运行的 Flutter Web 应用程序。使用 Chrome DevTools 为 Web 应用程序生成时间线事件。
使用 DevTools
Flutter DevTools 是一种可用来调试和分析 Flutter 应用程序的工具。DevTools 可用于实时查看应用程序的性能,并识别潜在的性能瓶颈。
在真机上测试应用程序
在真机上测试应用程序对于真实了解其性能非常重要。模拟器和模拟工具对于开发很有用,但它们可能无法准确反映应用程序在真机上的性能。
监视应用程序的性能
发布应用程序后,监视其性能非常重要。这将帮助你识别可能出现的任何性能问题,并采取措施来解决它们。
最小化高消耗操作
一些操作比其他操作更耗费资源,这意味着它们会消耗更多资源。显然,只有在必要时你才希望使用这些操作。应用程序 UI 的设计和实现方式会对运行效率产生巨大影响。
控制 build() 成本
在设计 UI 时,需要牢记以下几点:
- 避免在 build() 方法中进行重复且耗时的操作,因为 build() 可能会在祖先组件重建时频繁调用。
- 避免使用 build() 函数过大的单一大型组件。根据封装以及它们的更改方式将它们拆分成不同的组件:
- 当
setState()
在State
对象上调用时,所有子组件都会重建。因此,将setState()
调用定位到实际上需要更改 UI 的子树部分。如果更改包含在树的一个小部分中,请避免在树中较高位置调用setState()
。 - 当遇到与前一帧相同的子组件实例时,重建所有子组件的遍历停止。在框架内,此技术大量用于优化动画,动画不会影响子树。请参阅 TransitionBuilder 模式和 SlideTransition 源代码,它使用此原则避免在动画时重建其子组件。(“相同实例”使用 operator == 进行评估,但请参阅此页面结尾处的陷阱部分,了解何时避免覆写 operator == 的建议)。
- 尽可能在组件上使用
const
构造函数,因为它们可以让 Flutter 绕过大部分重建工作。要自动收到在可能的情况下使用const
的提醒,请在 flutter_lints 包中启用推荐 lints。 - 要创建可重复使用的 UI 部分,建议使用 StatelessWidget 而不是函数。
有关更多信息,请查看:
- StatefulWidget API 文档的中的性能注意事项
- Widgets vs helper methods,官方 Flutter YouTube 频道的一个视频,解释了为什么组件(特别是具有
const
构造函数的组件)的性能比函数更好。
慎重使用 saveLayer()
一些 Flutter 代码使用 saveLayer(),这是一个昂贵的操作,在 UI 中实现各种视觉效果。即使你的代码没有明确调用 saveLayer(),你使用的其他组件或包也可能会在后台调用它。也许你的应用程序调用的 saveLayer() 过于频繁;对 saveLayer() 的过多调用可能会导致卡顿。
为什么 saveLayer 很昂贵?
调用 saveLayer() 会分配一个离屏缓冲区,而将内容绘制到离屏缓冲区可能会触发渲染目标切换。GPU 希望像消防软管一样运行,而渲染目标切换会迫使 GPU 暂时重定向该流,然后再次导向回它。在移动 GPU 上,这对渲染吞吐量的影响尤为严重。
何时需要 saveLayer?
在运行时,如果你需要动态显示来自服务器的各种形状(例如),每个形状都有一些透明度,这些形状可能会(或可能不会)重叠,那么你几乎必须使用 saveLayer() 了。
调试对 saveLayer 的调用
你怎么能判断你的应用程序直接或间接调用 saveLayer() 的频率?saveLayer() 方法在 DevTools 时间表 上触发一个事件;通过在 DevTools Performance 视图 中查看 PerformanceOverlayLayer.checkerboardOffscreenLayers
切换按钮,了解你的场景何时使用 saveLayer()。
最小化对 saveLayer 的调用
你能避免对 saveLayer 调用吗?这可能需要重新考虑创建视觉效果的方式:
- 如果调用来自你的代码,你能减少或消除它们吗?例如,你的 UI 可能重叠两个形状,每个形状都具有非零的透明度
- 如果它们总是以相同的方式、相同的量、相同的透明度重叠,你可以预先计算重叠的半透明对象的外观,缓存它,并使用它而不是调用 saveLayer()。这适用于可以预先计算的任何静态形状。
- 你能重构你的绘图逻辑以完全避免重叠吗?
- 如果调用来自你不拥有的包,请与包所有者联系,并询问为什么需要这些调用。可以减少或消除它们吗?如果没有,你可能需要找到另一个包,或自己编写一个包。
其他可能触发 saveLayer() 并可能很昂贵的组件:
- ShaderMask
- ColorFilter
- Chip——如果
disabledColorAlpha != 0xff
,可能会触发对 saveLayer() 的调用 - Text——如果有
overflowShader
,可能会触发对 saveLayer() 的调用
最小化不透明度和剪切的使用
不透明度是一种耗费资源的操作,裁剪也是如此。以下是一些你可能会觉得有用的提示:
- 仅在必要时使用 Opacity 组件。请参阅
Opacity
API 页面中的 透明图像 部分,了解将不透明度直接应用于图像的示例,这比使用Opacity
组件更快。 - 不要将简单的形状或文本包裹在
Opacity
组件中,通常直接用半透明颜色绘制它们更快。(但这仅适用于要绘制的形状中没有重叠的片段时)。 - 要实现图像中的渐隐效果,请考虑使用 FadeInImage 组件,它使用 GPU 片段着色器应用渐进式不透明度。有关更多信息,请查阅 Opacity 文档。
- 剪切 不会调用 saveLayer()(除非使用
Clip.antiAliasWithSaveLayer
明确请求),因此这些操作不像不透明度那么耗费资源,但剪切仍然很昂贵,因此请小心使用。默认情况下,将禁用剪切(Clip.none
),因此在需要时你必须显式启用它。 - 要创建具有圆角的矩形,请考虑使用许多组件类提供的
borderRadius
属性,而不是应用剪切矩形。
慎重实现 Grids 和 Lists
Grids 和 Lists 的实现方式可能会导致你的应用程序出现性能问题。本节介绍了在创建网格和列表时的一个重要最佳实践,以及如何确定你的应用程序是否使用了过多的布局通道。
勤于利用延迟加载!
在构建大型网格或列表时,请使用带有回调的延迟构建方法。这可确保在启动时仅构建屏幕上的可见部分。
有关更多信息和示例,请查看:
- Cookbook 中的 Working with long lists
- 创建一次加载一页的 ListView:一篇 AbduRahman AlHamali 撰写的社区文章
- Listview.builder API
避免使用内部属性
有关内部属性可能如何引起网格和列表问题的信息,请参阅下一节。
最小化由内部操作引起的布局传递
如果你完成了许多 Flutter 编程,你可能熟悉在创建 UI 时 布局和约束如何工作。你甚至可能记住了 Flutter 的基本布局规则:约束向下。尺寸向上。父级设置位置。
对于某些组件,特别是网格和列表,布局过程可能会很耗费资源。Flutter努力只进行一次小部件的布局传递,但有时,需要第二次传递(称为内部传递),这可能会降低性能。
什么是内部传递?
当你希望所有单元格都具有最大或最小单元格的大小(或需要轮询所有单元格的类似计算)时,就会发生内部传递。
例如,考虑一个大的卡片网格。网格应该有统一大小的单元格,所以布局代码从网格的根(在小部件树中)开始进行传递,要求网格中的每个卡片(不仅仅是可见的卡片)返回其内部大小——小部件在没有约束的情况下的首选大小。有了这个信息,框架确定一个统一的单元格大小,并再次访问所有网格单元格,告诉每个卡片应使用的大小。
调试内部传递
要确定你是否有过多的内部传递,启用 DevTools 中的Track layouts option(默认关闭),并查看应用程序的堆栈跟踪以了解执行了多少次布局传递。一旦启用追踪,内部时间线事件将被标记为$runtimeType intrinsics
。
避免内部传递
你可以通过以下几种方式来避免内部传递:
- 将单元格预先设置为固定大小。
- 选择一个特定的单元格作为“锚定”单元格——所有单元格都将相对于此单元格进行大小设置。编写一个自定义的渲染对象,首先定位子锚定,然后在其周围布局其他子对象。
要更深入地了解布局的工作原理,可以查看 Flutter 架构概述 中的 布局和渲染 部分。
在 16 毫秒内构建并显示帧
由于构建和渲染有独立的两个线程,你在 60Hz 屏幕上有 16 毫秒用于构建,16 毫秒用于渲染。如果延迟是个问题,则在 16 毫秒或更短的时间内构建和显示一个帧。请注意,这意味着在 8 毫秒或更短的时间内构建,在 8 毫秒或更短的时间内呈现,总计 16 毫秒或更短。
如果你的帧在 profile 模式下的总渲染时间远低于 16 毫秒,即使存在一些性能缺陷,你可能也不必担心性能问题,但你仍然应该尽量快速地构建和渲染一个帧。为什么?
- 将帧渲染时间降至 16 毫秒以下可能不会带来视觉差异,但可以 延长电池续航时间并解决 热量问题。
- 它可能在你的设备上运行良好,但请考虑你定位的最低设备的性能。
- 随着 120fps 设备的普及,为了提供最流畅的体验,你需要在不到 8 毫秒(总计)的时间内渲染帧。
如果你想知道为什么 60fps 会带来流畅的视觉体验,请查看视频 Why 60fps?
陷阱
如果你需要调整应用程序的性能,或者 UI 或许不像预期的那样流畅,DevTools Performance 视图 可以提供帮助!
此外,适用于你的 IDE 的 Flutter 插件可能也很有用。在 Flutter Performance 窗口中,启用 Track Widget Builds 复选框。此功能可帮助你检测什么时候以超过 16 毫秒的时间渲染和显示帧。在可能的情况下,该插件会提供一个与相关提示的链接。
以下行为可能会对你应用程序的性能产生负面影响。
- 避免使用
Opacity
组件,尤其不要在动画中使用它。改用AnimatedOpacity
或FadeInImage
。有关更多信息,请查看 不透明度动画的性能注意事项。 - 使用
AnimatedBuilder
时,避免在构建器函数中放置一个构建器,该构建器构建不依赖于动画的组件。该组件的子树对于动画的每一次滴答都将被重建。相反,一次构建该组件的这部分,并将其作为子级传递给AnimatedBuilder
。有关更多信息,请查看 性能优化。 - 避免在动画中剪切。如果可能,在对其进行动画处理之前预剪切图像。
- 避免使用带有具体
List
子级的构造函数(例如,Column()
或ListView()
),如果大多数子级在屏幕上不可见,则避免构建成本。 - 避免在
Widget
对象上重写operator ==
。虽然它似乎可以通过避免不必要的重建来提供帮助,但在实践中它会损害性能,因为它会导致 O(N²) 行为。此规则的唯一例外是叶级组件(没有子级的组件),在特定情况下,比较组件的属性可能比重建组件并罕见的情况更有可能更改配置。即使在这些情况下,最好依赖缓存组件,因为即使一个operator ==
的重写也可能导致整个性能下降,因为编译器不再可以假定调用始终是静态的。
性能 FAQ
-
哪些性能仪表板具有与 Flutter 相关的指标?
-
如何向 Flutter 添加基准?
-
有一些用于获取和分析性能指标的工具吗?
-
我的 Flutter 应用看起来很卡顿。如何修复它?
-
有哪些需要小心对待的昂贵性能操作?
-
如何判断我的 Flutter 应用中的哪些组件在每一帧中都重建?
- 设置
debugProfileBuildsEnabled
在widgets/debug.dart
中为 true。 - 或者,可以将
widgets/framework.dart
中的performRebuild
函数更改为忽略debugProfileBuildsEnabled
,并始终调用Timeline.startSync(...)/finish
。 - 如果你使用 IntelliJ,则可以使用此数据的 GUI 视图。选择 跟踪小部件重建,在你的 IDE 中显示哪些小部件被重建。
- 设置
-
如何查询目标每秒帧数(屏幕)?
-
如何通过昂贵的 Dart 异步函数调用来解决我的应用程序中由昂贵的 Dart 异步函数调用引起的较差动画,该调用会阻塞 UI 线程?
- 使用
compute()
方法生成另一个隔离区,正如 在后台解析 JSON 烹饪书中所示。
- 使用
-
如何确定用户将下载的 Flutter 应用程序的包大小?
- 参见 测量应用程序的大小
-
如何查看 Flutter 引擎大小的细分?
- 访问 二进制大小仪表板,并用最新提交哈希替换 URL 中的 git 哈希 GitHub 引擎存储库提交。
-
如何截取正在运行的应用程序的屏幕截图并将其导出为 SKP 文件?
- 运行
flutter screenshot --type=skia --observatory-uri=...
- 注意查看屏幕截图的已知问题:
- 问题 21237:不会在真实设备中记录图像。
- 要分析和可视化 SKP 文件,请查看 Skia WASM debugger。
- 运行
-
如何从设备中检索着色器持久缓存?
-
在安卓系统中,你可以这样做:
1 2 3 4 5 6 7
adb shell run-as <com.your_app_package_name> cp <your_folder> <some_public_folder, e.g., /sdcard> -r adb pull <some_public_folder/your_folder>
-
-
如何在 Fuchsia 中执行追踪?
- 查看 Fuchsia 追踪准则