零:前言
可能说起 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
监管不到的重绘场景,以及对应的解决方案。