写点什么

关于 StatefulWidget,你不得不知道的原理和要点!

作者:岛上码农
  • 2022 年 7 月 26 日
  • 本文字数:2778 字

    阅读完需:约 9 分钟

关于 StatefulWidget,你不得不知道的原理和要点!

前言

对于一开始接触 Flutter 的人而言,会觉得 Flutter 挺简单的,一旦“搞清楚”了 StatelessWidgetStatefulWidget 后就上手开发了。一般这种情况在写写 Demo 的时候都没什么问题 —— 使用 setState 就搞定了界面的更新,可是页面和业务逻辑一旦复杂起来就会遇到很多性能问题,这个时候往往对这类问题不知道从而下手去解决。因此,有必要对 StatefulWidget 做一次深入的认识。

StatefulWidget 的分类

StatefulWidget 实际上也分两种类型,那就是是否会调用 setState 方法刷新界面。如果你使用 StatefulWidget 只是因为有些属性需要在 initState 中进行初始化,那么这样的 StatefulWidget 的开销很小,大可以放心大胆地使用。但是,如果你会频繁地调用 setState 进行界面刷新,那么就要小心了!很多性能问题都是由于 setState 导致的。

StatefulWidget 的渲染机制

首先说明一下,Flutter 是按照一帧一帧渲染的,因此一旦你的界面发生了变化,实际上就是更换了显示帧。当然,为了性能,Flutter 会尽可能地利用之前的渲染元素,渲染元素也就是 RenderObject。我们以一个官方视频 (油管)简单的例子来说明一下整个 StatefulWidgetsetState 的时候的渲染过程变化。


class ItemCounter extends StatefulWidget {  ItemCounter({Key? key, required this.name}) : super(key: key);  final String name;
@override _ItemCounterState createState() => _ItemCounterState();}
class _ItemCounterState extends State<ItemCounter> { int count = 0; @override Widget build(BuildContext context) { return GestureDetector( child: Text('${widget.name}: $count'), onTap: () { setState(() { count++; }); }, ); }}
复制代码


我们在之前的篇章有讲到实际上渲染控制是通过 Element Tree 完成的,Widget Tree 只是提供配置信息。对于上面的这个例子,实际上 State 对象对外是不可访问的,这个对象实际是被 StatefulElement 持有的。


StatefulElement(StatefulWidget widget)      : state = widget.createState(),//...
复制代码


整个组件的各个元素的对应关系如下图所示(以下图片均来自原视频),而 StatefulElement 实际渲染的元素也是一个 StatelessElement。上面的例子一开始其实就是 Text(Tom:0)这么一个渲染的组件(GestureDetector 并不是渲染元素),而此时 State 中的 count 的值是 0。



现在来看看当我们点击触发 count 增加的时候 1 的时候是什么样。当 count 变化的时候,我们知道会重新调用 build 方法,此时会出现一个 Text(Tom: 1)新的 Widget,这个时候 Flutter 会在下一帧到来的时候丢弃旧的 Text(Tom:0),在 WidgetTree 里取而代之的是 Text(Tom: 1)这么一个新的 Widget。但是,从性能考虑,由于 Flutter 检测到前后两个 Widget 对应的组件类型相同,因此会直接将 StatefulElement 更新指向到新的 Widget,而不是构建一个新的 StatefulElement。下面两张图展示了整个过程。




这里还有个需要注意的事项,也就是如果这个组件树中的某个节点类型虽然没有改变,但是相同位置替换为新的同类型组件(Widget,那么即便是属性变化了,Flutter 也不会移除 StatefulElement 创建新的对象,而是更新原先的StatefulElement 指向到新 Widget。 例如,我们将之前的 ItemCounter(name: 'Tom')换成了 ItemCounter (name: 'Dan'),这个变化过程可以用下面 4 张图表示。






这个过程可以通过 didUpdateWidget 的生命周期函数反映出来。


@overridevoid didUpdateWidget(covariant ItemCounter oldWidget) {  super.didUpdateWidget(oldWidget);}
复制代码

StatefulWidget 使用注意事项

有了上面的分析,我们再来看官方对于需要调用 setState 刷新的 StatefulWidget 的使用建议就很好理解了,以下是重点,开发时务必注意


  • 尽可能将组件树的状态维护往下推到叶子节点。这个很好理解,状态维护层级越高,意味着重建的组件树越大。当然是将状态维护放在低层级的叶子节点性能更高。举个例子,假设你页面中有一个每秒定时更新的时钟组件,那么这个时间状态的维护应该单独抽出一个时钟组件,由它自己维护时间状态。

  • 最小化 StatefulWidgetStatebuild 方法构建的组件的数量。理想情况下,一个 StatefulWidget 应该只有一个子组件,且这个组件对应一个 RenderObject。这个在现实中可能很难满足,但是这是一个指导原则,如果你的 StatefulWidget 的构建了太多组件,那么性能自然而然会下降。这个时候应该要考虑使用状态管理插件进行局部刷新了。

  • 如果子组件树在整个生命周期都不改变的话,那么应该考虑将该子组件树缓存起来重复利用。这会比每次重新构建这个子组件树性能好很多。通常的做法是将这个子组件树单独抽离为一个 Widget,然后作为子组件传给 StatefulWidget

  • 尽可能地使用 const 修饰子组件构造方法。这个我们在讲 const 时(解密 Flutter 的 const 关键字)有介绍过。实际上,使用 const 修饰相当于是一种缓存方式。

  • 尽可能避免更改子组件树的层级或子组件树中的组件类型。例如在返回子组件时,有可能会根据条件返回子组件或将子组件包裹在 IgnorePointer 中。这种方式其实就是改变了子组件树的层级。应该将子组件统一包裹在 IgnorePointer 中,然后通过IgnorePointerignoring 属性来控制。这是因为,任何更改子组件树深度的操作都会需要重新构建、重新布局、重新绘制整个子组件树。而只更改某个节点的属性的话,将会将改变的范围缩小很多(例如这个例子中,就不需要重新布局和重绘)。

  • 假设不得不更改子组件树的层级,那么应该考虑将子组件树中不变的部分使用 GlobalKey 使得这部分在整个 StatefulWidget 的生命周期都保持一致。如果不方便使用 GlobalKey 的话,那么可以考虑使用 KeyedSubtree 组件来应用 GlobalKey

  • 如果 StatefulWidget 中有些属性是不变的话,那么 这些属性的定义应该优先放在 Widget 的定义中,并声明为 final,而不是 State 中,这样可以减少 State 需要维护的数据。

总结

其实这篇的内容大部分都来自官方文档和视频,但是我估计做 Flutter 开发的看过的并不多。我们在接触一门新技术的时候,往往是先跑通 Demo,然后就看例程开始做开发。这样初期开发速度确实快,但是遇到问题的时候往往会一头雾水。因此,有 3 个建议:


  • 如果时间允许,一开始就多看看官方文档、相关周边的应用及说明文档,这样会减少后续很多的开发成本和时间——尤其是遇到性能瓶颈需要重构的时候。

  • 如果时间不允许,那么遇到问题的时候不要盲目折腾,回到官方文档先看几遍,Flutter 的官方文档非常详尽,而且还配套了很多讲解视频,能够解决你大部分困惑。

  • 多输入,虽然国内 Flutter 的玩家不太多,但是国外的话还是很多的,坚持逛逛国外的博客,官网说明,能够让你少走很多弯路。


欢迎关注个人公众号:岛上码农,或加本人微信:island-coder

发布于: 刚刚阅读数: 3
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
关于 StatefulWidget,你不得不知道的原理和要点!_flutter_岛上码农_InfoQ写作社区