前言
对于一开始接触 Flutter 的人而言,会觉得 Flutter 挺简单的,一旦“搞清楚”了 StatelessWidget
和 StatefulWidget
后就上手开发了。一般这种情况在写写 Demo 的时候都没什么问题 —— 使用 setState
就搞定了界面的更新,可是页面和业务逻辑一旦复杂起来就会遇到很多性能问题,这个时候往往对这类问题不知道从而下手去解决。因此,有必要对 StatefulWidget
做一次深入的认识。
StatefulWidget 的分类
StatefulWidget
实际上也分两种类型,那就是是否会调用 setState
方法刷新界面。如果你使用 StatefulWidget
只是因为有些属性需要在 initState
中进行初始化,那么这样的 StatefulWidget
的开销很小,大可以放心大胆地使用。但是,如果你会频繁地调用 setState
进行界面刷新,那么就要小心了!很多性能问题都是由于 setState
导致的。
StatefulWidget 的渲染机制
首先说明一下,Flutter 是按照一帧一帧渲染的,因此一旦你的界面发生了变化,实际上就是更换了显示帧。当然,为了性能,Flutter 会尽可能地利用之前的渲染元素,渲染元素也就是 RenderObject
。我们以一个官方视频 (油管)简单的例子来说明一下整个 StatefulWidget
在 setState
的时候的渲染过程变化。
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
的生命周期函数反映出来。
@override
void didUpdateWidget(covariant ItemCounter oldWidget) {
super.didUpdateWidget(oldWidget);
}
StatefulWidget 使用注意事项
有了上面的分析,我们再来看官方对于需要调用 setState
刷新的 StatefulWidget
的使用建议就很好理解了,以下是重点,开发时务必注意。
- 尽可能将组件树的状态维护往下推到叶子节点。这个很好理解,状态维护层级越高,意味着重建的组件树越大。当然是将状态维护放在低层级的叶子节点性能更高。举个例子,假设你页面中有一个每秒定时更新的时钟组件,那么这个时间状态的维护应该单独抽出一个时钟组件,由它自己维护时间状态。
- 最小化
StatefulWidget
的State
中build
方法构建的组件的数量。理想情况下,一个StatefulWidget
应该只有一个子组件,且这个组件对应一个RenderObject
。这个在现实中可能很难满足,但是这是一个指导原则,如果你的StatefulWidget
的构建了太多组件,那么性能自然而然会下降。这个时候应该要考虑使用状态管理插件进行局部刷新了。 - 如果子组件树在整个生命周期都不改变的话,那么应该考虑将该子组件树缓存起来重复利用。这会比每次重新构建这个子组件树性能好很多。通常的做法是将这个子组件树单独抽离为一个
Widget
,然后作为子组件传给StatefulWidget
。 - 尽可能地使用
const
修饰子组件构造方法。这个我们在讲const
时(解密 Flutter 的 const 关键字)有介绍过。实际上,使用const
修饰相当于是一种缓存方式。 - 尽可能避免更改子组件树的层级或子组件树中的组件类型。例如在返回子组件时,有可能会根据条件返回子组件或将子组件包裹在
IgnorePointer
中。这种方式其实就是改变了子组件树的层级。应该将子组件统一包裹在IgnorePointer
中,然后通过IgnorePointer
的ignoring
属性来控制。这是因为,任何更改子组件树深度的操作都会需要重新构建、重新布局、重新绘制整个子组件树。而只更改某个节点的属性的话,将会将改变的范围缩小很多(例如这个例子中,就不需要重新布局和重绘)。 - 假设不得不更改子组件树的层级,那么应该考虑将子组件树中不变的部分使用 GlobalKey 使得这部分在整个
StatefulWidget
的生命周期都保持一致。如果不方便使用GlobalKey
的话,那么可以考虑使用KeyedSubtree
组件来应用GlobalKey
。 - 如果
StatefulWidget
中有些属性是不变的话,那么 这些属性的定义应该优先放在Widget
的定义中,并声明为final
,而不是State
中,这样可以减少State
需要维护的数据。
总结
其实这篇的内容大部分都来自官方文档和视频,但是我估计做 Flutter 开发的看过的并不多。我们在接触一门新技术的时候,往往是先跑通 Demo,然后就看例程开始做开发。这样初期开发速度确实快,但是遇到问题的时候往往会一头雾水。因此,有3个建议:
- 如果时间允许,一开始就多看看官方文档、相关周边的应用及说明文档,这样会减少后续很多的开发成本和时间——尤其是遇到性能瓶颈需要重构的时候。
- 如果时间不允许,那么遇到问题的时候不要盲目折腾,回到官方文档先看几遍,Flutter 的官方文档非常详尽,而且还配套了很多讲解视频,能够解决你大部分困惑。
- 多输入,虽然国内 Flutter 的玩家不太多,但是国外的话还是很多的,坚持逛逛国外的博客,官网说明,能够让你少走很多弯路。
欢迎关注个人公众号:岛上码农,或加本人微信:island-coder。