(译) StatefulWidget 与性能

Flutter StatefulWidget 与应用性能其实有些微妙的联系。

Flutter 官方文档提到 widget 只是一个配置数据结构,创建是非常轻量的,加上 Flutter 团队对 widget 的创建/销毁做了优化,不用担心整个 widget 树重新创建所带来的性能问题。但实际上并非这么简单。看看这篇文章怎么说吧。

本文翻译自 Performance considerations

有两大类 StatefulWidget

一类 StatefulWidget 在 State.initState 方法中分配资源,并且在 State.dispose 方法中释放资源,但并不依赖 InheritedWidget 或调用 State.setState 方法。这类 Widget 通常用于应用或页面的根节点,并且使用 ChangeNotifierStream 或其他对象来跟子节点通信。遵守这种模式的 StatefulWidget 在 CPU 和 GPU 上的开销相对较低,因为它们只被创建一次且不再更新。所以这种 StatefulWidget 可以有相对复杂和较深层级的 build 方法。

另一类 StatefulWidget 使用 State.setState 方法或依赖 InheritedWidget。这类 widget 通常在应用的生命周期中重建很多次,因此最小化其重建过程非常重要。(它们可能也会在 State.initStateState.didChangeDependencies 方法中分配资源,但重点在于重建)

有若干技巧用来减小重建 StatefulWidget 对性能的影响:

  • 将状态下沉到叶子节点。比如,页面上有个时钟,与其将时间状态放在上层页面中(每次时间更新时重建整个页面),不如创建一个专用的 clock widget (每次时间更新时仅仅只需更新这个 widget)
  • 最小化 build 方法创建的 widget 的数量以及相对应的 node 数量。理想状态下,一个 StatefulWidget 只用创建唯一一个 widget (类型为 RenderObjectWidget)。(显然这并不总是可行,但 widget 越接近这个理想状态则越高效)
  • 如果一棵子树并不变化,缓存对应的 widget 并尽可能复用。复用一个 widget 比新生成一个 widget 要高效。一种常用的改进方式是,重构有状态的部分到一个接收 child 参数的 widget 中
  • 尽可能使用 const widget (这等同于缓存和复用 widget)
  • 避免改变已创建的子树的深度或修改子树中 widget 的类型。比如,与其直接返回子节点或将其包裹在 IgnorePointer,不如总是将子节点包裹在 IgnorePointer 并且操作 IgnorePointer.ignoring 属性值。原因是改变子树深度会导致重新 build/layout/paint 整个子树,而修改属性值只会对渲染树进行尽可能少的修改 (在 IgnorePointer 这个例子中,根本不必重新 layout 和 paint)
  • 如果出于某些原因必须要改变深度,可以考虑将子树的通用部分包裹在一个带 GlobalKey 的 widget 中 (该 GlobalKey 在 widget 生命周期中不变。如果其他 widget 不能很方便地设置 key,可考虑使用 KeyedSubtree)

总结

  • 减少 widget 重建 (可借助 Android Studio > Flutter Performance > Widget rebuild stats 来统计重建次数)
  • 缓存不变的子树
  • 尽可能使用 const widget