Flutter中的Tree(三) RenderObjectTree
什么是RenderObject
RenderObject, 从字面意思来理解, 就是可渲染的元素. 事实上, 每一个RenderObject都对应一个页面实际展示的元素. 它描述着这个元素的三大类属性: 如何绘制, 如何摆放, 能否被响应点击. 所以, 简单来说, RednerObject的职责有三个: Paint, Layout, Hit Test.
关于Hit Test的部分, 可以看本站的另一篇文章: 链接. 这篇文章只注重介绍Paint和Layout的部分.
RenderObject的分类
照例, 我们还是先看一下RenderObject的继承树. RenderObject有四个子类, RenderView是特殊的RenderObject, 它是Render Tree的根节点; RenderSilver是所有SilverWidget对应的RenderObject的父类; RenderAbstractViewport是所有ScrollWidget对应的RenderObject的父类; RenderBox则是剩下的所有RenderWidget对应的RenderObject的父类. RenderObject作为基类, 并没有定义Layout时要用的坐标系(是极坐标还是笛卡尔坐标), 也没有定义放置child的排版策略(width-in-height-out还是constraint-in-size-out). 具体使用哪种, 是由它的子类根据自己的需要来指定的.
RenderObject的核心成员变量和方法
ParentData
Element篇中我们提到ParentElement会通过_applyParentData(RenderObject object)方法讲ParentData赋值给所有Child RenderObjectElement的RenderObject. 很多RenderObject会在layout或者paint过程中使用这个数据.
markNeedsLayout()和RelayoutBoundary
markNeedsLayout(), 顾名思义, 就是将自己标记为需要重新layout. 同时, 因为自己需要layout了, 可能父节点也需要layout, 所以要继续调用父节点的markNeedsLayout()方法.
想也知道, 在这种机制下, 子节点一旦需要layout, 整个树都要Layout, 这显然会影响性能. 所以, RenderObject定义了一个RelayoutBoundary概念, 即这个RenderObject的layout边界是哪个RenderObject. 在markNeedsLayout时, 只要layout事件传递到了RelayoutBoundary就停止, 这样就可以缩小影响的范围了. RenderObject的_relayoutBoundary属性会在每次parent对其layout时传递过来. parent在layout时, 会先判断自己是否可以成为RelayoutBoundary, 如果可以, 那么就将自己传递给child, 同时将自己的_relayoutBoundary属性设置成自己; 否则就将自己parent的给的_relayoutBoundary传递给child.
从layout()的源码看, 成为RelayoutBoundary, 需要满足下面四个条件之一:
parent在layout时不会使用自己的尺寸信息(也就是自己对parent无影响)
自己的尺寸完全由parent的约束决定, 即只要parent的约束不变, 自己的尺寸也不变
parent传给自己的约束是Tight,实际上和条件2是一样的, 只不过约束是一个定值
parent不是RenderObject. 这个条件是考虑到RenderView的child这个边界情况设置的
还要说的是, markNeedsLayout()方法的调用时机, 除了被child调用外, 还有下面三种情况:
RenderObject被添加到RenderObject Tree上时
RenderObject的child发生增删移时
RenderObject自己的布局属性发生变化时(比如parentData变了这样的)
performLayout()和performResize()
performLayout()和performResize()方法是RenderObject定义的模板方法. 各个RenderObject子类通过实现这两个方法, 完成对child的排版和对自己尺寸的计算(只有自己的尺寸计算完了, parent才可以排版自己).
需要指出的是, 只有sizedByParent为true, 即自己的尺寸完全由parent的约束决定时, 自己的尺寸才会通过performResize()方法计算出来; 否则, 自己的尺寸计算应该由performLayout在获得了child的尺寸后进行计算. 换句话说, parent传递下来的约束优先级是最高的(红屏的原因就在这里).
layout()
layout(Constraints constraints, { bool parentUsesSize = false })方法是layout流程的入口方法, 由PipelineOwner调用RelayoutBoundary的performLayout()方法, 再由RelayoutBoundary在自己的performLayout()方法中调用child的layout(Constraints constraints, { bool parentUsesSize = false })方法. 实际上, layout流程的实现逻辑是放在performLayout()方法中的. layout()方法只是做了一些断言, 另外就是计算和更新RelayoutBoundary.
markNeedsPaint()和RepaintBoundary
在了解了markNeedsLayout()和RelayoutBoundary后, markNeedsPaint()和RepaintBoundary也就可以类比了, 所以这里不再赘述.
paint()
在介绍paint(PaintingContext context, Offset offset)方法前, 我们要先简单介绍一下作为参数的PaintingContext.
PaintingContext是Flutter Framework提供的一个绘制管理器, 我们可以把它理解为windows上的画图应用. PaintingContext持有三个类的实例: PictureLayer, Canvas和PictureRecorder. PictureLayer就像画图应用中的画布; Canvas就像画图应用中的画笔; PictureRecorder提供了应用的保存功能. 当绘制操作结束后, 可以通过其endRecording()方法得到一个Picture对象. 这个对象可以被送到Engine层进行光栅化, 最后渲染到屏幕上. 当然这中间还有一些步骤, 我们放到下篇文章再说.
最后我们简单提一下ContainerLayer. 就像画图程序一样, PaintingContext除了绘制外, 也提供了其它的图像操作功能, 比如剪裁, 翻转等. 画图功能对应的是PictureLayer, 剪裁对应的是另外一种Layer…… 这些操作(Layer)的集合才算是真正的绘制结果. ContainerLayer就是承载结果的容器. 虽然每个RenderObject都有一个ContainerLayer的成员变量_layer. 但是一般情况下, 只有RepaintBoundary会被系统分配一个ContainerLayer. 当然有时候为了一些特殊的效果, 我们希望一些图形画在另外的一张图层上, 这在Flutter中被叫做Compositing. PaintingContext也提供了新建图层的功能. 当一个不是RepaintBoundary的RenderObject需要画在新的图层上时, PaintingContext会为其分配一个Layer, 同时创建一个新的PaintingContext实例与其相关.
关于Layer, 我们会在下一篇文章继续介绍.
PipelineOwner
在Element篇中, 我们介绍了收集dirty Element的BuildOwner, PipelineOwner其实是RenderObject版的BuildOwner: 它收集需要重新排版和绘制的RenderObject, 在下一帧时对这些RenderObject进行对应的处理.
同BuildOwner一样, PipelineOwner也是从根节点开始向下传递继承的.
实际上, PipelineOwner收集四类dirty RenderObject:
Layout:RenderObejct需要重新layout
Compositing Bits Update:RenderObejct需要(或者不再需要)Compositing
Paint:RenderObejct需要重新paint
Semantics:RenderObject的辅助信息有变化
每一帧开始时, PipelineOwner会按照上述顺序, 依次处理这些RenderObject.
实际上, 处理这四类RenderObject的逻辑都相同: 从子节点开始, 从下往上重新执行流程(如调用RelayoutBoundary的performLayout()方法和调用RepaintBoundary的paint()方法).