
零:前言
可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint 组件,自定义 CustomPainter 对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint 组件来画的,其实 CustomPaint 组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试、调试及源码分析来给出一些在绘制时被忽略或从未知晓的东西,而有些要点如果被忽略,就很可能出现问题。
我只是一把刀,英雄可以拿我除暴安良,坏蛋可以拿我屠戮无辜。我会因英雄的善举而被赞美,也会因坏蛋的恶行而被唾弃。然而,我无法决定自己的好坏,毕竟我只是一把刀,一个工具。我只能祈祷着被他人的善用,仅此而已。 这就是 State#setState ,一个触发刷新的工具,它的好与坏,不是取决于它的本身,而是使用它的人。
注:文章结尾有总结,注意查收,毕竟正文不是每个人都能看完的。
这小结将通过一个测试来说明,在 Flutter 中的刷新时,什么在变,什么不在变。这对理解 Flutter 来说至关重要。此处用来一个最精简的 StatefulWidget 进行测试,效果如下:每 3 秒依次变色为 红黄蓝绿。

void main() => runApp(ColorChangeWidget());
class ColorChangeWidget extends StatefulWidget {
@override
_ColorChangeWidgetState createState() => _ColorChangeWidgetState();
}
class _ColorChangeWidgetState extends State<ColorChangeWidget> {
final List colors = [
Colors.red, Colors.yellow,
Colors.blue, Colors.green ];
Timer _timer;
int index = 0;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 5), _update);
}
void _update(timer) {
setState(() {
index = (index + 1) % colors.length;
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: ShapePainter(color: colors[index]),
);
}
}
复制代码这里自定义一个 StatefulWidget,使用 Timer.periodic 创建一个定时的计时器,每 3 秒触发一次,修改激活的索引,并执行 _ColorChangeWidgetState#setState 来重构界面。绘制还是由 ShapePainter 画个圈,使用 CustomPaint 进行显示。
class ShapePainter extends CustomPainter {
final Color color;
ShapePainter({this.color});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = color;
canvas.drawCircle(Offset(100, 100), 50, paint);
}
@override
bool shouldRepaint(covariant ShapePainter oldDelegate) {
return oldDelegate.color != color;
}
}
复制代码现在只在 ShapePainter#paint 方法上添加断点, 下面是两次 paint 时的情况。我们可以发现一个非常重要的地方,那就是 State#setstate 虽然会重建当前 build 方法下的节点,但是 RenderObject 对象是不会重建的,如下 RenderCustomPaint 的内存地址一直都是 #1dbcd。你可以放行断点,让颜色多变化几次,你会发现渲染对象的地址是一直保持不变的。


但有一个对象一直在变,那就是 ShapePainter 对象。从 _ColorChangeWidgetState#build 中也可以看到画板对象一直变化的原因,因为 State#setState 会触发 State#build ,而在 build 中 ShapePainter 是被重新实例化的。
---->[_ColorChangeWidgetState#build]----
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: ShapePainter(color: colors[index]),
);
}你也许会想,为什么不将 ShapePainter 作为成员变量,这样就不需要每次 build 都创建了。通过 Flutter 源码中对 CustomPainter 的使用可以知道,对应静态的绘制,画板类中的属性都是定义为 final ,也就是常量,是不允许修改属性的。这样即使 ShapePainter 为成员变量,也无法修改信息。此时 CustomPainter 就像Widget 一样只是一种配置的描述,是轻量的。

在第一篇也说过,对于有 滑动 或 动画 需求的绘制,重建触发的频率非常大,此时即使对象是 轻量的,也会在短时间内创建大量对象,这样不是很好。这时可以使用 repaint 属性来控制画板的刷新,做到在画板对象保存不变的情况下,刷新画板,其原理也在第三篇说过了。 所以对应静态的绘制而言外界的 State#setState,会让 Widget 、CustomPainter 这样的描述性信息对象重新创建。而真正承担绘制、布局的 RenderObject 对象还是同一个对象,这便是 铁打的营盘流水的兵。
setState 是 State 类中的成员方法,其中传入一个回调方法。经过断言后,会执行回调方法,并执行 _element.markNeedsBuild() 。可以看到 setState 方法主要就是执行这个方法,那 _enement 是什么呢?

每个 State 类都会持有 StatefulElement 和 StatefulWidget 对象,这里也就是执行 State 持有的这个Element 的 markNeedsBuild() 方法。

可以从变量面板看出,当前 _ColorChangeWidgetState 持有的 Widget 是 ColorChangeWidget,持有的 Element 是 StatefulElement 。现在也就是即将调用这个 Element 对象的 markNeedsBuild() 方法。

下一步就会进入 Element.markNeedsBuild,也就是 Element 类中。在两个小判断之后,该元素的 _dirty 属性被置为 true,也就是元素标脏。然后执行 owner.scheduleBuildFor(this),其中 owner 对象是 Element 的成员,其类型为 BuildOwner,注意方法的入参是 this,也就是该元素自身。

下一步将进入 BuildOwner.scheduleBuildFor ,如果 element 的 _inDirtyList 为 true,会直接返回。一般只有被加入 脏表集合 后才会置为 true , 如下 2590 行。当条件满足,会执行 onBuildScheduled 方法。

此时方法会进入 WidgetsBinding._handleBuildScheduled。也就是说 onBuildScheduled 是 BuildOwner 中的一个方法成员,在某个时刻被赋值成了 WidgetsBinding._handleBuildScheduled , 所以才会跑到这里。这里只是调用了一下 ensureVisualUpdate。

在 SchedulerBinding.ensureVisualUpdate 方法中会通过 scheduleFrame 来调度一个新帧。

在该方法里通过 window.scheduleFrame() 来请求新的帧,

Window#scheduleFrame 是一个 native 方法,通过注释可以知道,该方法会在下一次适当的时机调用onBeginFrame 和 onDrawFrame 回调函数。

之后方法进行完毕,一波退栈,回到了 BuildOwner.scheduleBuildFor。BuildOwner中有一个 _dirtyElements 列表用于存储脏元素。然后当前元素就被收录进去,并将 _inDirtyList 置为 true。setState 到这里就退栈了。

所以 State#setState 主要就做两件事:
1、通过 onBuildScheduled 触发帧的调度
2、将当前 State 持有的 Element 对象加入 BuildOwner 中的脏表集合虽然 setState 方法结束了,但它的余威还在。在触发帧的调度后,会触发帧的重新绘制,被表脏的元素也会触发 rebuild。还记得 BuildOwner 中维护的 _dirtyElements 脏表集合吧,BuildOwner 是用于负责管理和构建元素的类,每个帧的重绘都会走到这个方法中。现在在 BuildOwner.buildScope 打上断点,可以看到绘制帧的方法入栈情况。

一开始会判断 callback 是否为 null,且 _dirtyElements 是否为空,如果都满足的话,就说明不需要重建,直接返回。我们知道刚才由于 State#setState 方法,有一个元素被装进脏表中了,所有会继续执行。

这里会先通过 sort 对脏元素列表进行排序。

在这里会遍历 _dirtyElements 执行其中 element 的 rebuild 方法,那么好戏即将开始。

我们在任何时候都不能忘本,要时刻清楚 this 是什么,这是浩瀚源码之海中最亮的明灯。执行 rebuild 方法的,是之前被加入脏表的那个 StatefulElement,接下来会进入 Element.rebuild。因为 StatefulElement 中重写 rebuild ,使用才会到父类的方法中。可以看到 rebuild 方法中只是做了一断言而已,执行了 performRebuild。

然后进入到的是 StatefulElement.performRebuild,很明显,是由于 StatefulElement 重写了该方法。下面有一个比较重要的点:如果 _didChangeDependencies 为 true ,那么 _state 会触发 didChangeDependencies() 回调方法。

可以看出 StatefulElement 会持有 State 对象,而 State 对象又会持有 StatefulElement,从下面的图片可以看出当前对象类型,StatefulElement 和 _ColorChangeWidgetState 是互相持有的关系。

这里 _didChangeDependencies 为 false,然后会执行 super.performRebuild()。由于 StatefulElement 的父类是 ComponentElement,所以入栈方法如下:

继续向下,会发现有一个局部变量 built 会通过 build() 方法初始化。接下来,就是见证奇迹的时刻。

继续前进,这个 build 方法的实现是_state.build(this) ,这时你应该会恍然大悟,这句代码意味着什么。下一刻将会发生什么,这个 this 当前元素将要去往哪里。

这里的 _state 成员,我们已经知道了是 _ColorChangeWidgetState ,那么这个 build 方法,也就是我们写的构建组件。第一次,源码和我们写的东西出现了交集,而回调的 BuildContext 对象,就是那个 Element。如下,在 build 方法里 CustomPaint 和 ShapePainter 都被重新实例化了。它们已经不再是曾经的它们,它们如同草屑一般被抛弃,新的对象携带者新的配置信息,加入到了这一轮的构建。ShapePainter 的颜色此时会随着 index 变化而改变。

然后方法弹栈后,built 对象被赋值为刚才创建的 CustomPaint 对象,其持有的 ShapePainter 是下一个颜色,这样 built 对象成为了携带着新配置信息的打工人,开始了工作。

然后来到一个非常核心的方法 Element#updateChild。在进入这个方法之前,先梳理一下元素树的层级关系。目前元素树上只有 3 个元素,最顶层的是框架内部创建的 RenderObjectToWidgetElement ,第二个就是当前的 this----StatefulElement ,第三个是 CustomPaint 组件创建的 SingleChildRenderObjectElement 。如果对此有什么疑惑,可见第二篇。这里就是通过 built 这个新的Widget 对 _child 进行更新,这个 _child 就是第三节点 SingleChildRenderObjectElement

下面进入 Element.updateChild ,注意此时变量区的信息。
[1]. newWidget 也就是新创建的 含有新配置信息 的打工人。
[2]. this 是第二元素节点,也就是 updateChild 方法的调用者,一个 StatefulElement 对象
[3]. child 就是第三元素节点,那个待更新的孩子 SingleChildRenderObjectElement
[4]. 这里的返回值是为了更新 this 节点的 _child 属性,也就是更新 第三元素节点当 newWidget 为 null 时,会返回 null,且 child 不为 null 时,会被从树上移除。这里都非 null 会继续向下,声明一个 newChild 的局部变量,这里 child 非空。

继续向下,就是新旧打工人的比较,child.widget 持有的是之前的 CustomPaint ,newWidget 是新的,所以这个条件不满足。从这也可以看出,如果新旧 Widget 对象不变的话,会有优化,直接使用旧的孩子。

由于新旧 Widget 不是同一对象,就会走下面分支,判断 Widget 是否可以更新。可更新的条件是:新旧组件的运行时类型和 key 一致 ,这里是满足的,继续向下。

然后会执行 child.update(newWidget) ,使用新的配置信息来更新 child ,也就是 第三元素节点。

然后进入 SingleChildRenderObjectElement.update ,会执行 super 的 update 方法。

进入 RenderObjectElement.update 后,依然会执行 super.update,到达顶层的 Element#update,这里的操作仅是将 _widget 成员赋值为 newWidget。

然后 Element#update 出栈,回到 RenderObjectElement.update 方法,在这里执行了一个非常重要的方法 widget.updateRenderObject(this, renderObject)。这是希望你已经理解了前面的三篇文章,然后向下看,效果会更好。

然后会进入 CustomPaint.updateRenderObject 方法,对传入的 renderObject 进行属性的重设。这时你就可以发现,这个 renderObject 是被传入来的,所以该渲染对象并未被重新创建,这时对该对象的属性进行了设置。所以现在明白第一小结 铁打的营盘流水的兵 是什么意思了吧,配置信息相关的对象非常轻量,可以重新创建,而 RenderObject 是绘制的阵营,只要对配置信息进行重新设置即可。

到这里,你还记得在 RenderCustomPaint 中 set painter 会怎么样吗?第三篇有说。会触发 _didUpdatePainter 方法。

然后根据 shouldRepaint 来决定在画板重设时,是否需要触发重绘。所以 shouldRepaint 只有在外界迫 使 RenderCustomPaint 重新设置 painter 时才会触发,其中最常见的就是外界的 State#setState。所以 shouldRepaint 把守的是这道门。

在两个画板不同时,通过 markNeedsPaint 将自己加入 PipelineOwner 的待绘制列表,等待重绘。

RenderObject 更新完后,方法依次出栈,会到 RenderObjectElement.update ,将 _dirty 置为false,便出栈。

然后 SingleChildRenderObjectElement 依然会更新它的孩子,由于这里它的 child 是 null ,方法执行完依然是 null。

第三元素节点更新后,方法退回到 ComponentElement.performRebuild ,此时的 _child 所持有 RenderObject 对象已经使用新的配置更新完毕,并加入了待重新渲染的列表。也就是说,使用 setState 进行更新,只是轻量级的配置信息创新创建,而 Element 、RenderObject 、State 这样的对象不会重新创建,只是根据配置信息进行了更新。

更新完毕,退栈到 BuildOwner.buildScope进行首尾工作,清空脏表。

接下来 BuildOwner.buildScope 出栈 RendererBinding.drawFrame 入栈,之后的事就是绘制了。这个方法应该已经非常熟悉了。在第三篇有详细说 pipelineOwner.flushPaint() 方法,这里就不再说明了。

最终,会触发 ShapePainter#paint 进行绘制。这就是在 setState 时进行的 Element 重新构建 和 RenderObject 的更新。我们应该已经了解到,一般情况下使用 setState 不会让 Element 和 RenderObject 重新创建,而是基于新的 Widget 配置信息进行更新。这差不多就是四两拨千斤吧。

从 Flutter 最初的时代,State#setState 如同神迹一般的存在,想刷新就用 setState 。以至于 State#setState 被滥用,各种时机的刷新满天飞。当认识到 ValueListenableBuilder 、FutureBuilder、StreamBuilder、AnimatedBuilder 这些组件的局部刷新,或者 Provider、Bloc 这样的状态管理提高的局部刷新组件,似乎让 State#setState 成为了闲谈中被口诛笔伐的对象,会发出这样的言论,这是很片面的。我只想说,和文章开头一样,State#setState 只是一个工具,工具没有好与坏。

通过上面的代码可以发现 State#setState 的作用是将持有的 Element 加入待构建的脏表,并触发帧的调度来重新构建和绘制。所以 State#setState 的好与坏取决于 Element 的层级,如果有人非要在高层级使用 State#setState 来刷新,说 State#setState 不好,就相当于残害无辜后说这把刀是恶刀一样。
setState 的封装ValueListenableBuilder 组件是监听对象变化使用 setState 进行重新构建的。

FutureBuilder 组件根据异步任务的状态,使用 setState 进行重新构建的。

StreamBuilder 组件根据 Stream 的状态,使用 setState 进行重新构建的。

AnimatedBuilder 组件也是监听动画器,使用 setState 进行重新构建的。

就算是状态管理 Bloc 的 BlocBuilder 也是依赖于 setState 进行重新构建的。

在 Provider 中,对刷新进行了一定的封装,但还是最终还是离不开 element#markNeedsBuild 。

所以说无论什么局部刷新,内部的原理都和 State#setState 是一样的。基本上都是对 setState 的一层封装。我们不能因为看不到 State#setState 的存在,就否定它的价值。就像一边让人家在底层干活,一边说着别人的坏话一样。对应 setState 我们要注意的是它刷新元素的层级,而不是否定它。
现在来终结一下 Custompainter#shouldRepaint 只是在当 RenderCustomPaint 设置画板属性的时候才会被回调。 RenderCustomPaint 设置画板属性的场景在于:其对应的 RenderObjectElement 触发 update 时,由 widget#updateRenderObject 方法进行属性设置,注意只是属性的设置,而非对象的重建。
---->[CustomPaint#updateRenderObject]----
@override
void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) {
renderObject
..painter = painter
..foregroundPainter = foregroundPainter
..preferredSize = size
..isComplex = isComplex
..willChange = willChange;
}而 RenderObjectElement 触发 update 触发基本上是由于外界执行 setState 方法。所以 shouldRepaint 的作用也是有局限性的。下一篇将一起探索 shouldRepaint 监管不到的重绘场景,以及对应的解决方案。