「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
Widget 渲染过程
Flutter 把视图数据的组织和渲染抽象为三部分,即 Widget,Element 和 RenderObject。
Flutter 渲染过程,可以分为这么三步:首先,通过 Widget 树生成对应的 Element 树;然后,创建相应的 RenderObject 并关联到 Element.renderObject
属性上;最后,构建成 RenderObject 树,以完成最终的渲染。
屏幕每秒刷新60帧,Flutter 第一次将页面绘制到屏幕上,它需要找出屏幕上每个元素的位置、颜色、文本等。也就是说,在第一次渲染中,需要配置屏幕上的每一个像素。
对于后续的屏幕刷新和绘制,如果没有任何更改,Flutter 会使用上一次的绘制信息,并快速的在屏幕上绘制出来。而当每次屏幕刷新都需要计算屏幕上的所有内容时,才会出现掉帧。
Widget
Widget 是对视图的一种结构化描述,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。
Widget 设计成不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 Widget 树的方式进行数据更新,以数据驱动 UI 构建的方式简单高效。虽然重建涉及到大量对象的销毁和重建,会对垃圾回收造成压力,不过,Widget 本身并不涉及实际渲染位图,所以它只是一份轻量级的数据结构,重建的成本很低。另外由于 Widget 的不可变性,可以以较低成本进行渲染节点复用,一个Widget对象可以对应多个 Element 对象。
Element
Element 是 Widget 的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。
Element 同时持有 Widget 和 RenderObject。而无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有 RenderObject。
因为 Widget 具有不可变性,但 Element 却是可变的。实际上,Element 树这一层将 Widget 树的变化(类似 React 虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。这,就是 Element 树存在的意义。
BuildContext 就是 Widget 对应的 Element。
Element 树不会在每次调用build(){...}
方法时重建。
RenderObject
从其名字,我们就可以很直观地知道,RenderObject 是主要负责实现视图渲染的对象。
Flutter 通过控件树(Widget 树)中的每个控件(Widget)创建不同类型的渲染对象,组成渲染对象树。而渲染对象树在 Flutter 的展示过程分为四个阶段,即布局、绘制、合成和渲染。 其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。
- 每当 Flutter 遇到一个之前没有被渲染的元素时,它就会通过 Widget 树中的配置,在元素树中创建一个元素。
- 渲染树也不会经常重建。
- 除了布局、绘制、合成和渲染阶段,它还有另一个阶段,将监听器附加到 Widget 上,这样我们就可以监听事件。
build()
的构建过程
每当状态发生变化时,Flutter 就会调用方法build()
。一般有两种出发重建的条件:
-
有状态的 Widget 中调用
setState(){...}
方法。会导致build(){...}
方法的调用。 -
其次,每当有
MediaQuery
调用或Theme.of(...)...
调用、软键盘出现/消失等,只要这些数据发生变化,就会自动触发build(){...}
方法。
调用setState(){...}
将相应的元素标记为 dirty 。对于下一次刷新(每秒发生60次),Flutter 会将build(){...}
方法创建的新配置进行分析,然后更新屏幕。
反最佳实践的例子
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
debugPrint('=======_MyHomePageState.build:');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
使用 Flutter 创建的计数器项目,然后添加几个text
,通常为了避免地狱回调,我们通常会把嵌套的组件抽离成一个方法, Flutter Outline 也有快捷操作:
_buildRow(),
// Row(
// children: <Widget>[
// Text('我是第一行'),
// Text('我是第二行'),
// Text('我是第三行'),
// ],
// ),
Row _buildRow() {
return Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
);
}
看着真棒,去掉了地域嵌套,方法行数减少,结构清晰。说真的,过去的一年多我都是这么干的。
问题
直到看到这个:
Wm 是 Flutter 的开发人员倡导者。
每当值发生变化时,Flutter 都会调用setState()
。这触发了 Widget 调用build()
方法重建。然后调用_buildRow()
重建这个方法返回的控件。_incrementCounter``build``_buildRow()``_counter
。
前面渲染流程我们说过,build()
的时候 Element 不一定重建。但是因为这个方法,会导致每次都会重建它。重建不需要重建的东西时浪费了宝贵的CPU周期。
解决方案
解决方案很简单:不是将构建方法拆分为更小的方法,而是将其拆分为小部件 - 无状态小部件。
上面的 Demo 最终是这样:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
debugPrint('=======_MyHomePageState.build:');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_NonsenseWidget(),
// _buildRow(),
// Row(
// children: <Widget>[
// Text('我是第一行'),
// Text('我是第二行'),
// Text('我是第三行'),
// ],
// ),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
Row _buildRow() {
return Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
);
}
}
class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
);
}
}
当是一个 Widget 时,Flutter 会对比是否需要重建,因为是 相同类型的 StatelessWidget,所以会复用。避免了重建。