【Flutter核心类分析】深入理解RenderObject
作者:互联网
文章目录
背景
Widget
,Element
,RenderObject
可以说是Flutter Framework的三个核心成员,本文我们一起来学习下RenderObject
。
宏观上来讲,一个RenderObject就是我们之前说的三棵树之一RenderObject Tree中的一个对象,它的职责主要有三个:布局,绘制,命中测试。其中命中测试在之前的文章一文深入了解Flutter事件机制有过详细的分析,本文我们主要来讲解其它两点:布局和绘制。
RenderObject分类
RenderObject本身是一个抽象类,其具体实现也是由其子类负责,我们先来看看它的分类:
如图所述RenderObject主要分类四类:
-
RenderSliver
RenderSliver是所有实现了滑动效果的RenderObject基类,其常用子类有RenderSliverSingleBoxAdapter等。
-
RenderBox
RenderBox是一个采用2D笛卡尔坐标系的RenderObject的基类,一般的RenderOBject都是继承自RenderBox,例如RenderStack等,它也是一般自定义RenderObject的基类。
-
RenderView
RenderView是整个RenderObject Tree的根节点,代表了整个输出界面。
-
RenderAbstractViewport
RenderAbstractViewport是一类接口,此类接口为只展示其部分内容的RenderObject设计。
下面我们从RenderObjct
生命周期的几个关键节点展开:创建,布局,渲染
创建
说道RenderObject的创建,相信看过之前文章:深入理解Widget,深入理解Element,都不会陌生,也就是当我们的Element被挂载(mount)到Element tree上时,会调用RenderObjectWidget.createRenderObject方法,创建RenderObjectElement,再调用Element.attachRenderObject方法,将其attach到RenderObject Tree上,也就是在Element的创建过程中RenderObject Tree被逐步创建出来。
@override
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
驱动
想要了解RenderObject的刷新原理,我们需要首先了解下Flutter是如何驱动刷新的,我们首先来看图:
- 当 RenderObject 需要重新 layout 时,调用
markNeedsLayout
方法,该方法会将当前 RenderObject 加入PipelineOwner#_nodesNeedingLayout
或传给父节点去处理; - 当 RenderObject 的 Compositing Bits 有变化时,调用
markNeedsCompositingBitsUpdate
方法,该方法会将当前 RenderObject 加入PipelineOwner#_nodesNeedingCompositingBitsUpdate
或传给父节点去处理; - 当 RenderObject 需要重新 paint 时,调用
markNeedsPaint
方法,该方法会将当前 RenderObject 加入PipelineOwner#_nodesNeedingPaint
或传给父节点处理; - 当 RenderObject 的辅助信息(Semantics)有变化时,调用
markNeedsSemanticsUpdate
方法,该方法会将当前 RenderObject 加入PipelineOwner#_nodesNeedingSemantics
或传给父节点去处理
上述就是 PipelineOwner 不断收集Dirty RenderObjects
的过程。
上述4个markNeeds*
方法,除了markNeedsCompositingBitsUpdate
,其他方法最后都会调用PipelineOwner#requestVisualUpdate
。之所以markNeedsCompositingBitsUpdate
不会调用PipelineOwner#requestVisualUpdate
,是因为其不会单独出现,一定是伴随其他3个之一一起出现的。
随着PipelineOwner#requestVisualUpdate
->RendererBinding#scheduleFrame
->Window#scheduleFrame
调用链,UI 需要刷新的信息最终传递到了 Engine 层。
具体讲,Window#scheduleFrame
主要是向 Engine 请求在下一帧刷新时调用Window#onBeginFrame
以及Window#onDrawFrame
方法。
从上面的图,我们看到每一帧的刷新都会调用到PipelineOwnder.flushLayout
,也就是对所有需要Layou对象的布局。
void flushLayout() {
if (!kReleaseMode) {
Timeline.startSync('Layout', arguments: timelineArgumentsIndicatingLandmarkEvent);
}
try {
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
} finally {
if (!kReleaseMode) {
Timeline.finishSync();
}
}
}
首先,PipelineOwner
对于收集到的 Needing Layout RenderObjects 按其在RenderObject Tree
上的深度升序排序,主要是为了避免子节点重复 Layout (因为父节点 layout 时,也会递归地对子树进行 layout);
其次,对排好序的且满足条件的 RenderObjects 依次调用_layoutWithoutResize
来执行 layout 操作。
驱动小结:
综合来说,就是先将需要layout的renderObject添加到_nodesNeedingLayout在调用scheduleFrame,下一帧来临时就会根据需要layout。layout的驱动流程分为两个:
-
第一次启动
第一次启动的入口肯定是在runApp()
-
启动之后UI变更Layout
之后的layout驱动就是上图的
markNeedsLayout
布局
从上面分析我们知道,当RenderObject需要(重新)布局的时候会调用markNeedsLayout
方法,从而被Owner._nodesNeedingLayout
收集,并且在下一帧的时候触发Layout操作,那么现在我们来看看markNeedsLayout
有哪些调用场景。
-
RenderObject被添加到RenderObject tree
在RenderObjectElement中的
attachRenderObject
方法内部会间接调用到ParentDataWidget的applyParentData
方法,在其方法内部会调用targetParent.markNeedsLayout()
-
子节点adopt、drop、move
@override void adoptChild(RenderObject child) { setupParentData(child); //这里 markNeedsLayout(); markNeedsCompositingBitsUpdate(); markNeedsSemanticsUpdate(); super.adoptChild(child); }
@override void dropChild(RenderObject child) { child._cleanRelayoutBoundary(); child.parentData!.detach(); child.parentData = null; super.dropChild(child); //这里 markNeedsLayout(); markNeedsCompositingBitsUpdate(); markNeedsSemanticsUpdate(); }
void move(ChildType child, { ChildType? after }) { final ParentDataType childParentData = child.parentData! as ParentDataType; if (childParentData.previousSibling == after) return; _removeFromChildList(child); _insertIntoChildList(child, after: after); //这里 markNeedsLayout(); }
-
由子节点的
markNeedsLayout
方法传递调用@protected void markParentNeedsLayout() { _needsLayout = true; final RenderObject parent = this.parent! as RenderObject; if (!_doingThisLayoutWithCallback) { //这里 parent.markNeedsLayout(); } else { } }
-
RenderObjectElement自身与布局相关属性发生变化的时候,如:
RenderPositionedBox
的widthFactor
double? get widthFactor => _widthFactor; double? _widthFactor; set widthFactor(double? value) { assert(value == null || value >= 0.0); if (_widthFactor == value) return; _widthFactor = value; //这里 markNeedsLayout(); }
当然上面markNeedsLayout
只是一个标记过程,标记之后,下一帧来临时候需要layout。
那么layou具体做些设么呢,我们还是需要从RenderObject.layout方法着手:
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject? relayoutBoundary;
// 下列四种情况下relayoutBoundary = this
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
//将父类的_relayoutBoundary赋值给relayoutBoundary
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
// 这里判断是否需要layout
// _needsLayout,如果是true,就会layout
// _constraints 是从父布局传递过来的约束信息,如果有变化的话就需要layout
// _relayoutBoundary 这只是flutter framework的优化措施如果!_needsLayout && constraints == _constraints两个都成立的话根据这个判断是否需要更新,下面重点分析
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
try {
performResize();
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
}
RenderObject? debugPreviousActiveLayout;
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
_needsLayout = false;
markNeedsPaint();
}
RenderObject设计有一个很重要的概念就是Relayout Boundary
,字面意思就是布局边界意思。Relayout Boundary 是一项重要的优化措施,可以避免不必要的 re-layout。它的主要表现就是在变量_relayoutBoundary上。当某个 RenderObject 是 Relayout Boundary 时,会切断 layout dirty 向父节点传播,即下一帧刷新时父节点无需 re-layout。那么什么情况下这个RenderObject就是RelayoutBoundary呢,上面源码的if语句中也描述了很清晰:!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject
-
parentUsesSize为false
即父节点在 layout 时不会使用当前节点的 size 信息(也就是当前节点的排版信息对父节点无影响);
-
sizedByParent为true
即当前节点的 size 完全由父节点的 constraints 决定,即若在两次 layout 中传递下来的 constraints 相同,则两次 layout 后当前节点的 size 也相同;
-
constraints.isTight为true
其效果与
sizedByParent
为true
是一样的,即当前节点的 layout 不会改变其 size,size 由 constraints 唯一确定; -
父节点不是 RenderObject:
谁的父节点不是RenderObject,想想就知道就是根节点了,它的父节点为null
sizedByParent
为true
的 Render Object 需重写performResize
方法,在该方法中仅根据constraints
来计算 size。如RenderBox
中定义的performResize
的默认行为:取constraints
约束下的最小 size就是Size.zero:
@override
void performResize() {
size = computeDryLayout(constraints);
}
@protected
Size computeDryLayout(BoxConstraints constraints) {
return Size.zero;
}
若父节点 layout 依赖子节点的 size,在调用layout
方法时需将parentUsesSize
参数设为true
。
因为,在这种情况下若子节点 re-layout 导致其 size 发生变化,需要及时通知父节点,父节点也需要 re-layout (即 layout dirty 范围需要向上传播)。这一切都是通过上节介绍过的 Relayout Boundary 来实现。
本质上,layout
是一个模板方法,具体的布局工作由performLayout
方法完成。RenderObject#performLayout
是一个抽象方法,子类需重写。
关于performLayout
有几点需要注意:
- 该方法由
layout
方法调用,在需要 re-layout 时应调用layout
方法,而不是performLayout
; - 若
sizedByParent
为true
,则该方法不应改变当前 Render Object 的 size ( 其 size 由performResize
方法计算); - 若
sizedByParent
为false
,则该方法不仅要执行 layout 操作,还要计算当前 Render Object 的 size; - 在该方法中,需对其所有子节点调用
layout
方法以执行所有子节点的 layout 操作,如果当前 Render Object 依赖子节点的布局信息,需将parentUsesSize
参数设为true
。
下面看看RenderFlow的performLayout
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
size = _getSize(constraints);
int i = 0;
_randomAccessChildren.clear();
RenderBox? child = firstChild;
while (child != null) {
_randomAccessChildren.add(child);
final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);
child.layout(innerConstraints, parentUsesSize: true);
final FlowParentData childParentData = child.parentData! as FlowParentData;
childParentData.offset = Offset.zero;
child = childParentData.nextSibling;
i += 1;
}
}
- 对所有子节点逐个调用
layout
方法; - 计算当前 Render Object 的 size;
- 将与子节点布局有关的信息存储到相应子节点的
parentData
中。
绘制
与markNeedsLayout
相似,当 Render Object 需要重新绘制 (paint dirty) 时通过markNeedsPaint
方法上报给PipelineOwner
。在同样调用owner.requestVisualUpdate();驱动布局绘制流程
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();
}
}
markNeedsPaint
内部逻辑与markNeedsLayout
都非常相似:
- 若当前 Render Object 是 Repaint Boundary,则将其添加到
PipelineOwner#_nodesNeedingPaint
中,Paint request 也随之结束; - 否则,Paint request 向父节点传播,即需要 re-paint 的范围扩大到父节点(这是一个递归的过程);
- 有一个特例,那就是『 Render Object Tree 』的根节点,即 RenderView,它的父节点为 nil,此时只需调用
PipelineOwner#requestVisualUpdate
即可。
PipelineOwner#_nodesNeedingPaint
收集的所有 Render Object 都是 Repaint Boundary。
跟上面布局中提到的Relayout Boundary一样,绘制中也有一个同样的设计Repaint Boundary,根据上面对Relayout Boundary分析知道若某 Render Object 是 Repaint Boundary,其会切断 re-Paint request 向父节点传播。
更直白点,Repaint Boundary 使得 Render Object 可以独立于父节点进行绘制,否则当前 Render Object 会与父节点绘制在同一个 layer 上。总结一下,Repaint Boundary 有以下特点:
- 每个 Repaint Boundary 都有一个独属于自己的 OffsetLayer (ContainerLayer),其自身及子孙节点的绘制结果都将 attach 到以该 layer 为根节点的子树上;
- 每个 Repaint Boundary 都有一个独属于自己的 PaintingContext (包括背后的 Canvas),从而使得其绘制与父节点完全隔离开。
Flutter Framework 为开发者预定义了
RepaintBoundary
widget,其继承自SingleChildRenderObjectWidget
,在有需要时我们可以通过RepaintBoundary
widget 来添加 Repaint Boundary。
上面的分析也都是一个标记的过程,标记为需要重新绘制的时候等下一帧来临时候就会重新刷新当前元素。具体的刷新动作我们还是要根据RenderObject.paint方法来分析:
void paint(PaintingContext context, Offset offset) { }
抽象基类RenderObject
中的paint
是个空方法,需要子类重写。
paint
方法主要有2项任务:
-
当前 Render Object 本身的绘制,如:
RenderImage
,其paint
方法主要职责就是 image 的渲染@override void paint(PaintingContext context, Offset offset) { if (_image == null) return; _resolve(); assert(_resolvedAlignment != null); assert(_flipHorizontally != null); paintImage( canvas: context.canvas, rect: offset & size, image: _image!, debugImageLabel: debugImageLabel, scale: _scale, colorFilter: _colorFilter, fit: _fit, alignment: _resolvedAlignment!, centerSlice: _centerSlice, repeat: _repeat, flipHorizontally: _flipHorizontally!, invertColors: invertColors, filterQuality: _filterQuality, isAntiAlias: _isAntiAlias, ); }
-
绘制子节点,如:
RenderTable
,其paint
方法主要职责是依次对每个子节点调用PaintingContext#paintChild
方法进行绘制:@override void paint(PaintingContext context, Offset offset) { //...... for (int index = 0; index < _children.length; index += 1) { final RenderBox? child = _children[index]; if (child != null) { final BoxParentData childParentData = child.parentData! as BoxParentData; context.paintChild(child, childParentData.offset + offset); } } //...... }
文章小结
本文对Flutter UI的布局,绘制驱动做了详细的分析,通过流程图能够很清晰的看到Flutter framework的调用流程,同时还对RenderObject的Relayout Boundary、Repaint Boundary 这两个概念做了重点分析。
标签:RenderObject,调用,layout,节点,深入,child,Flutter,constraints 来源: https://blog.csdn.net/huangbiao86/article/details/121482190