零:前言
1. 系列引言
可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint 组件,自定义 CustomPainter 对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint 组件来画的,其实 CustomPaint 组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试、调试及源码分析来给出一些在绘制时被忽略或从未知晓的东西,而有些要点如果被忽略,就很可能出现问题。
2.前情回顾
希望在观看此篇前,你已经看过前面文章的铺垫 。上回说到与 CustomPainter 关系最为密切的是 RenderCustomPaint 这个渲染对象。我们都知道,通过 CustomPainter#paint 方法可以获取到 Canvas 对象进行绘制操作,但你有么有想过,这个 Canvas 是从何而来的?CustomPainter#paint 方法又是在哪里回调的?shouldRepaint 到底是在哪里起的作用?这些都会在本文的探索中给出答案。
一、CustomPainter#paint 方法探索
1. 测试代码
为了更方便探索 CustomPainter 的内部机制,这里使用最精简的代码,摒除其余干扰信息。如下代码直接将 CustomPaint 组件传给 runApp 方法,运行效果如下:
void main() => runApp(CustomPaint(
painter: ShapePainter(color: Colors.blue),
));
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;
}
}
2. paint 方法的调试分析
想要进行分析,最有效的方式便是 调试,在 paint 方法添加断点,调试信息如下。左侧是程序运行到 paint 时方法栈帧情况,当前 ShapePainter.paint 方法处于栈顶,其下的方法都是在方法栈中还未执行完毕的方法,它们都在等着栈顶的方法退栈。所以可以从这里看出方法依次进栈的顺序,从而很快了解 paint 是如何一步步被调用的。
RenderCustomPaint._paintWithPainter 在 ShapePainter.paint 之下,说明 ShapePainter.paint 是在该方法里被调用的。如下所示,点击栈帧中的方法时,会进行跳转。来到 RenderCustomPaint 类中的 _paintWithPainter 方法内,ShapePainter.paint 被调用的那一行,这就是 debug 的强大之处。
通过调试可以看到方法栈的调用情况,但很多方法在一块,会让人觉得很乱,有时走着走着自己就乱了,不知道在干嘛。所以在调试中有件一个很重要的事:就是认清我是谁,我在哪里,我要干什么,这让你不会迷路。我们可以通过栈帧看到当前方法所处的位置;另外,任何方法调用时,都是一个对象在调用,这个对象便是 this,当我们迷路时,this 会成为指路明灯。通过下面计数器的图标,可以输入表达式和查看对象信息。查看 this 信息如下,当前对象为 RenderCustomPaint 类型,可以看到当前对象的成员信息。
这时我们知道了 ShapePainter.paint 是在 RenderCustomPaint._paintWithPainter 中被调用的,那么 _paintWithPainter 又是在哪调用的呢。同理,可以看下一个栈帧。它是在 RenderCustomPaint.paint 中被触发的。也就是说 RenderCustomPaint 作为一个 RenderObject 本应要处理绘制的任务,但是它将这个任务向外界暴露出去,由用户进行绘制处理。
而暴露给用户的抽象层便是 CustomPianter,可以看出 CustomPianter#paint 回调的出去的 Canvas 是 RenderCustomPaint#paint 方法参数的 PaintingContext 中的 canvas 对象。
3.RendererBinding.drawFrame
在 runApp 方法中,会执行 WidgetsFlutterBinding#scheduleWarmUpFrame 开始调度绘制帧。
每次帧的回调会触发 RendererBinding#_handlePersistentFrameCallback 。在此方法中会执行 drawFrame。至于 Flutter 框架层如何启动,初始化各个 Binding ,如何添加 _handlePersistentFrameCallback 回调的,本文就不详述了,着重在绘制的点。
---->[RendererBinding#_handlePersistentFrameCallback]----
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
}
这样我们方法 RendererBinding.drawFrame ,它的作用就是绘制帧。在这里触发了PipelineOwner.flushPaint ,从而吹响了绘制的号角。
PipelineOwner 中持有 _nodesNeedingPaint 对象,它是一个 RenderObject 列表,收集需要绘制的 RenderObject。在 PipelineOwner.flushPaint 中,会对收集到需要绘制的 RenderObject 使用 PaintingContext.repaintCompositedChild 静态方法进行绘制。可以看出当前的节点是 RenderView,它的孩子是 RenderCustomPaint 这也就是当前 渲染树 的结构。RenderView 是在 Flutter 框架内部初始化的RenderObject, 它永远都是渲染树的根节点。
PipelineOwner 类中在允许绘制之前还有几个条件,1. 渲染对象的 _layer 属性非空;2. 渲染对象的 _needsPaint 属性为 true ;3.渲染对象持有的 PipelineOwner 为当前对象;4. 渲染对象的 _layer 成员的 _ower 非空。
---->[PipelineOwner#flushPaint]----
assert(node._layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
---->[AbstractNode#attached]----
bool get attached => _owner != null;
可以回想一下上文中,RenderObject 对象的 markNeedsPaint 方法,就是在向 owner._nodesNeedingPaint 列表中添加渲染对象 。下面是 RenderObject#markNeedsPaint 去除断言后的所有代码。可以看出,自己 在被加入到owner 的待渲染列表前,会有些条件。1. _needsPaint 属性为 false。 2. isRepaintBoundary 为 true。否则就让 父节点执行 markNeedsPaint。
所以从这里可以看出:当一个 RenderObject 对象执行 markNeedsPaint 时,如果自身 isRepaintBoundary 为false,会向上寻找父级,直到有 isRepaintBoundary=true 为止。然后该父级节点被加入 _nodesNeedingPaint 列表中。
---->[RenderObject#markNeedsPaint]----
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) {
if (owner != null) {
owner!._nodesNeedingPaint.add(this); //<--- 自己被加入 待渲染列表
owner!.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent as RenderObject;
parent.markNeedsPaint();
} else {
if (owner != null)
owner!.requestVisualUpdate();
}
}
repaintCompositedChild 是 PaintingContext 的静态方法,没有复杂的逻辑,只是调用了 _repaintCompositedChild。
---->[PaintingContext#repaintCompositedChild]----
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
assert(child._needsPaint);
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
4. 绘制上下文 PaintingContext 的诞生
在 _repaintCompositedChild 方法中除去断言后,所有代码如下:可以看到这里创建了 PaintingContext ,也就是 Canvas 的发源地。这里的 child 对象便是根渲染节点 RenderView。可以看出 PaintingContext 类只是用于提供绘制的上下文,最终的绘制还是由 RenderObject 自身完成。
---->[PaintingContext#_repaintCompositedChild]----
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
child._layer = childLayer = OffsetLayer();
} else {
childLayer.removeAllChildren();
}
childContext ??= PaintingContext(child._layer!, child.paintBounds);//绘制上下文的创建
child._paintWithContext(childContext, Offset.zero); // RenderObject 绘制
childContext.stopRecordingIfNeeded();
}
在 RenderObject#_paintWithContext 方法中做了很多断言的操作,其本身并没有什么复杂的逻辑,就调用了一下该类的 paint 方法,将上面传来的绘制上下文回调出去。
---->[RenderObject#_paintWithContext]----
void _paintWithContext(PaintingContext context, Offset offset) {
if (_needsLayout)
return;
RenderObject? debugLastActivePaint;
_needsPaint = false;
try {
paint(context, offset); // <--- 调用 paint
} catch (e, stack) {
_debugReportException('paint', e, stack);
}
}
在 RenderView.paint 方法中,会触发 PaintingContext.paintChild 方法。然后会触发渲染树下一节点的绘制。我们知道,下一个节点就是 RenderCustomPaint。
从这里可以看出,如果 child.isRepaintBoundary 为 true 就不会触发 child 的绘制,而是使用 _compositeChild 进行合成,将 child._layer 添加到 _containerLayer 中,这样可以避免渲染对象的绘制。如果 child.isRepaintBoundary 为 false,会执行 _paintWithContext 方法进行绘制,也就是当前的情况。
---->[RenderObject#paintChild]----
void paintChild(RenderObject child, Offset offset) {
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
}
void _compositeChild(RenderObject child, Offset offset) {
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
}
final OffsetLayer childOffsetLayer = child._layer as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(child._layer!); // 添加到 _containerLayer 中
}
@protected
void appendLayer(Layer layer) {
layer.remove();
_containerLayer.append(layer);
}
这样一来,一条路就畅通了,现在可以自己回味一下从 RendererBinding.drawFrame 一路过来发生的事情。多调试调试,栈帧,会为你诉说它所经历的 故事。
当前的渲染树只有 RenderView 和 RenderCustomPaint 两个节点。在绘制时 RenderView.paint 先入栈 , RenderCustomPaint.paint 后入栈,这说明在前面的节点会一直等待后面的节点绘制完毕,自己的绘制才算结束。现在让当栈帧依次出栈,当 pipelineOwner.flushPaint() 执行完毕,屏幕上就会出现绘制的图形。这么我们就了解了一下 CustomPainter#paint 是什么时候被调用的,以及 Canvas 对象是何时被创建的。
二、 CustomPainter#shouldRepaint 方法探索
1.源码中对 shouldRepaint 的使用
遇事不决,先看源码,源码中 20 个基于 CustomPainter 绘制的组件,我们可以从其中来看到正规的适用方式。那个简单的 _GridPaperPainter 来看,它在 shouldRepaint 中进行的处理是: 只要属性成员和旧的画板对象有所不同,就返回 true 。 如果完全一致,则返回 false。这基本上是作为画板而言,刻在 DNA 里的操作了。
2. 从源码认识 shouldRepaint
CustomPainter#shouldRepaint 在整个 Flutter 框架中只有两处使用。第一个是在 CustomPaintershouldRebuildSemantics 中,会默认调用它来进行判断。
第二个就是在 RenderCustomPaint#_didUpdatePainter 中 ,这个方法的触发,是在为 RenderCustomPaint 设置新画板 时。这里的 oldPainter 也就是之前的画板。
set painter(CustomPainter? value) {
if (_painter == value)
return;
final CustomPainter? oldPainter = _painter;
_painter = value;
_didUpdatePainter(_painter, oldPainter);
}
我们来仔细看一下 _didUpdatePainter 这个方法,入参是新旧两个画板。[1]. 如果新画板为 null ,重新绘制来清除旧画。[2]. 如果新画板为 null 、新旧画板运行时类型不一致、shouldRepaint 返回值为 true ,这三个条件满足其一,就可以通过 markNeedsPaint 让 RenderCustomPaint 加入重绘渲染列表。
void _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter) {
if (newPainter == null) {
markNeedsPaint();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRepaint(oldPainter)) {
markNeedsPaint();
}
if (attached) {
oldPainter?.removeListener(markNeedsPaint);
newPainter?.addListener(markNeedsPaint);
}
if (newPainter == null) {
if (attached)
markNeedsSemanticsUpdate();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRebuildSemantics(oldPainter)) {
markNeedsSemanticsUpdate();
}
}
到这里再来回答,shouldRepaint 返回 false,就一定不会重绘当前画板吗?答案以及很明显了。并非全然,一者 oldPainter == null 和 newPainter.runtimeType != oldPainter.runtimeType 两个条件如果满足也是可以的。但不要忽略一个要点,这个方法只是在 set painter 时被触发。还有别的情况可能引起绘制对象重绘,比如父级渲染对象的刷新、_painter 基于监听器的刷新,这些是 shouldRepaint 无法控制的。
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_painter?.addListener(markNeedsPaint);
_foregroundPainter?.addListener(markNeedsPaint);
}
所以 shouldRepaint 并非是一个控制画板刷新的万金油。我们需要根据情况进一步处理,至于怎么处理,在上面我们讲到过 RenderObject 中有一个属性可以控制重绘,它就是 isRepaintBoundary。现在对于 CustomPainter 最核心的两个方法已经介绍完毕,你应该可以回答出本篇一开始的那几个问题了。在下一篇我们将进一步去探索 Flutter 绘制的奥秘,在什么情况下会触发 shouldRepaint 无法控制的刷新,我们又该如何去控制。
@张风捷特烈 2021.01.11 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~