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

简介: 改善 StatefulWidget性能,你不得不知道这些!从原理说明 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。

image.png

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

image.png

image.png

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

image.png

image.png

image.png

image.png

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

@override
void 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

相关文章
|
1月前
|
存储 Kubernetes NoSQL
k8s学习--资源控制器StatefulSet详细解释与应用
StatefulSet 是 Kubernetes 中用于管理有状态应用的控制器,提供稳定的网络标识符和持久化存储能力。相较于 Deployment、DaemonSet 等无状态服务控制器,StatefulSet 支持有状态服务如 MySQL、Redis 等集群的部署和管理。本文详细介绍 StatefulSet 的概念、特点及部署方法,并通过具体示例展示了如何配置 NFS 存储及 StatefulSet 应用,确保每个 Pod 拥有独立且持久的数据存储空间。
|
Kubernetes Go Docker
【k8s 系列】k8s 学习二十六-3,Statefulset 实战 1
上一部分与大家分享到 Statefulset 与 RplicaSet 的区别,以及 Statefulset 的特点,能做的一些事情及一些注意事项
|
6月前
Flutter StatefulWidget传递数据,多级控件传递数据
Flutter StatefulWidget传递数据,多级控件传递数据 在Flutter中,StatefulWidget可以通过构造函数将数据传递给其子控件,这种方式适用于一些简单的场景。但是,当存在多级嵌套控件时,将数据从祖先传递到后代可能会变得困难。在这种情况下,可以使用Flutter提供的InheritedWidget类来传递数据。
113 0
|
Kubernetes 索引 Perl
【k8s 系列】k8s 学习二十六-5,Statefulset 实战 3
上一部分我们说到如何使用 Statefulset 部署有状态的应用,Statefulset 可以做到部署的 每一个 pod 能够独立的拥有一个持久卷声明和持久卷
|
存储 Kubernetes 索引
【k8s 系列】k8s 学习二十六-2,Statefulset 部署应用
上一部分我们分享到了使用 RS 没有办法让自己管理的多个 pod 都有一个独立的持久化声明,RS 没有办法在指定模板中对不同的 pod 做差异化处理
146 0
|
XML 存储 Dart
Flutter 基础 | 自定义控件 StatelessWidget & StatefulWidget
Flutter 基础 | 自定义控件 StatelessWidget & StatefulWidget
322 0
|
Java 数据处理 开发者
Preference组件探究之自定义Preference
Preference组件探究之自定义Preference
Preference组件探究之自定义Preference
|
存储 Dart 监控
Flutter(二)之有状态的StatefulWidget
在开发中,某些Widget情况下我们展示的数据并不是一层不变的: 比如Flutter默认程序中的计数器案例,点击了+号按钮后,显示的数字需要+1; 比如在开发中,我们会进行下拉刷新、上拉加载更多,这时数据也会发生变化; 而StatelessWidget通常用来展示哪些数据固定不变的,如果数据会发生改变,我们使用StatefulWidget;
336 1
Flutter(二)之有状态的StatefulWidget
|
XML API Android开发
Preference组件探究之源码解读
Preference组件探究之源码解读
|
Dart Android开发
【Flutter】StatefulWidget 组件 ( 创建 StatefulWidget 组件 | MaterialApp 组件 | Scaffold 组件 )(一)
【Flutter】StatefulWidget 组件 ( 创建 StatefulWidget 组件 | MaterialApp 组件 | Scaffold 组件 )(一)
185 0
【Flutter】StatefulWidget 组件 ( 创建 StatefulWidget 组件 | MaterialApp 组件 | Scaffold 组件 )(一)