本文是
Flutter Framework 渲染流程分析中的第三篇
- Flutter Framework 渲染流程分析(一):开篇
- Flutter Framework 渲染流程分析(二):Element
- Flutter Framework 渲染流程分析(三):RenderObject
- Flutter Framework 渲染流程分析(四):Layer
- Flutter Framework 渲染流程分析(五):常见问题分析
相较于Widget和Element,RenderObjet则要复杂的多。界面的布局(Layout)、绘制(Paint)、语义化(Semantics)都在RenderObject中完成,同时它持有一个布局的核心:Layer,Layer Tree组成后构成Scene传到Engine最终输出成界面像素。
由于篇幅限制,本文只关注RenderObject中Layout、Paint流程,Layer会在之后的篇幅详细解析。
RenderObject
首先来看一下RenderObject下的子类

RenderObject继承自AbstractNode,我们在第一篇讲过AbstractNode是树节点的抽象基类。RenderObject下大概可以分为三类:
RenderView:比较特殊,它是RenderObject Tree的根元素,只会有一个实例。RenderSliver:是所有 Sliver 类型组件的基类。RenderBox:是其它组件的基类。
Layout
我们先来看跟Layout流程有关的属性和接口
dart
// code 3.1
[RenderObject] bool _needsLayout = false;
[RenderObject] RenderObject? _relayoutBoundary;
[RenderObject] void markNeedsLayout()
[RenderObject] void markParentNeedsLayout()
[RenderObject] void layout(Constraints constraints, { bool parentUsesSize = false })
[RenderObject] void _layoutWithoutResize()
[RenderObject] void performResize()
[RenderBox] void performResize()
[RenderObject] void performLayout()
仔细看这些方法,有三种操作。
- 以
mark开头的方法:主要用来对一些属性进行标记,这里指_needsLayout、_relayoutBoundary; - 以
layout开头的方法:根据属性的标志判断是否进行perform的操作; - 以
perform开头的方法:真正的处理,这里有两个不同的操作:resize和layout;
下面我们通过源码去了解这些方法。
void markNeedsLayout()
dart
// code 3.2
void markNeedsLayout(){
...
if (_needsLayout) {
...
return;
}
// 正常情况下,走到 markNeedsLayout 时 _relayoutBoundary 都是有值的
if (_relayoutBoundary == null) {
// relayoutBoundary 可能被父元素通过 _cleanRelayoutBoundary 方法给清掉了
_needsLayout = true;
if (parent != null) {
...
markParentNeedsLayout();
}
return;
}
if (_relayoutBoundary != this) {
// 当前不是布局边界时,调用父元素 markNeedsLayout
markParentNeedsLayout();
} else {
// 当前是布局边界,标记 _needsLayout
_needsLayout = true;
if (owner != null) {
...
// 同时将脏节点添加到 owner 中,并请求刷新
// 后面 Mark to Flush 会讲到刷新过程,暂时不用在意
ownner!._nodesNeedingLayout.add(this)
ownder!.requestVisualUpdate();
}
}
}
markNeedsLayout 过程其实就是将节点添加到脏列表(_nodeNeedsLayout),并请求刷新的过程。 我们再来看这个 _nodeNeedsLayout,通过定位它的调用,发现它有两个add操作,一个是在上述markNeedsLayout;另外一个是在RenderView的scheduleInitialLayout(),也就是根节点中。无论是RenderView还是上述的markNeedsLayout中的节点,它都是布局边界元素。
何为布局边界元素?一个元素如果它的_relayoutBoundary == this的话,它就是布局边界元素。
通常意义来讲,布局边界元素形成一个独立的布局区域,它们之间的布局是不互相影响的。而在这个独立的布局区域内的元素,一个元素的布局信息改变了,它里面的所有元素的布局都可能被影响到,因此标脏时要同步到布局边界上。
void markParentNeedsLayout()
dart
// code 3.3
void markParentNeedsLayout() {
...
// 标记
_needsLayout = true;
...
final RenderObject parent = this.parent! as RenderObject;
...
// 执行父元素的 markNeedsLayout,
parent.markNeedsLayout();
...
}
markParentNeedsLayout 比较简单,就是让父元素调用markNeedsLayout,结合 code 3.2 中的代码,其实就是寻找上层的布局边界元素并进行标脏的操作。
void layout(Constraints constraints, { bool parentUsesSize = false })
dart
// code 3.4
void layout(Constraints constraints, { bool parentUsesSize = false }) {
...
// 布局边界的判定
final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;
...
if (!_needsLayout && constraints == _constrains) {
// !_needsLayout 表示元素没有经过 markNeedsLayout,如果它的布局约束也没有变的话,
// 元素的布局信息是没有改变的,无需进行后续的布局计算,这个条件里最后直接 return
...
// 如有需要,更新一下 _relayoutBoundary
if (relayoutBoundary != _relayoutBoundary) {
_relayoutBoundary = relayoutBoundary
// 当 _relayoutBoundary 改变的时候,更新 children 的 _relayoutBounary
// 注意,_progateRelayoutBoundaryToChild 是递归处理,
// 直到 child 的 _relayoutBoundary 没有发生变更
visitChildren(_propagateRelayoutBoundaryToChild);
}
...
return;
}
_constaints = constraints;
...
_relayoutBoundary = relayoutBoundary;
...
// sizedByParent 为 true 的情况下,会走 performResized
if (sizedByParent) {
...
try {
performResize();
...
} catch(e, stack) {
_reportException('performResize', e, stack);
}
...
}
...
try {
performLayout();
markNeedsSemanticsUpdate();
...
} catch(e, stack) {
_reportException('performLayout', e, stack);
}
...
_needsLayout = false;
markNeedsPaint();
...
}
我们先看一下布局边界的判断bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject。
parentUsesSize是方法参数,默认为 false;sizedByParent表示节点的 size 是不是仅仅由布局constraints决定。子类可以覆写,默认为 false;constraints也是方法传参,constraints.isTight表示约束有固定的宽高,比如宽高都定为 200;parent is! RenderObject只有一种情况,当前是RenderView,此时parent为空,RenderView属于布局边界元素;
这样那么一个节点可以通过以下几种方式确定为布局边界元素:
- 是根节点
RenderView - 这个
RenderObject类型就是布局边界:覆写RenderObject中的sizedByParent,返回 true - 布局时由上层确定:调用 layout 时的传参
parentUsesSize和constraints决定
在调用 performLayout之前,如果sizedByParent为 true,会多走一个performResize方法,最后则走调到markNeedsPaint。
总结就是:layout方法要做的事就是先计算出是否是布局边界以及当前的边界元素。通过这两个条件去更新自己和 children 的_relayoutBoundary,_constraints的更新也在这里进行。同时根据条件进行performResize操作,接着走完performLayout后更新标志位_needLayout为false,最后markNeedsPaint。
void _layoutWithoutResize()
dart
// code 3.5
...
try {
performLayout();
markNeedsSemanticsUpdate();
} catch(e, stack) {
_reportException('performLayout', e, stack);
}
...
_needsLayout = false;
markNeedsPaint();
与layout方法相比,_layoutWithoutResize不走performResize,并且调用它的地方只有一个:[pipelineOwner].flushLayout(),flushLayout是遍历_nodesNeedingLayout调用node._layoutWithoutResize。前面已经提到,_nodesNeedingLayout都是布局边界元素,并且已经标志了_needsLayout,所以就无需像layout那样的前面先做一些布局边界的判断。可以看出,_layoutWithoutResize是简化版的layout。
接下来看真正的布局计算方法。
void performResize()
当sizedByParent为 true 时,才会走到 performResize。 code 3.4 中也提到过 sizedByParent 为 true 时表示节点的 size 仅由布局约束constraints决定,所以performResize方法实际上就是通过constraints计算出_size的过程。
RenderObject中performResize是一个抽象方法,没有实现。我们来看子类RenderBox的实现。
dart
// code 3.6
/// [RenderBox]中的实现
void performResize() {
size = computeDryLayout(constraints);
...
}
computeDryLayout看子类RenderViewport的实现
dart
/// [RenderViewport]中的实现
Size computeDryLayout(BoxConstraints constraints) {
...
// 取约束中的最大值
return constraints.biggest;
}
简而言之,performResize就是通过constraints计算size的过程。
void performLayout()
跟performResize一样,RenderObject中performLayout交由子类去实现。为了方便理解,我们取一个实现简单的类RenderConstraintBox去看。
dart
// code 3.7
/// [RenderConstraintBox]
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentusesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
performLayout主要处理两个事,布局child,计算自身的size。当存在child时,RenderConstraintBox本身的size会依赖于child.size,所以首先要计算child的布局;否则就根据_additionalConstraints和constraints计算size。
Layout 总结
通过调用layout/_layoutWithoutResize触发performResize/performLayout计算布局信息(比如size、padding、offset等),同时也会调用child的layout去计算底下子孙元素的布局,这一个layout是不断向下触发的,当然也会有一个拦截的条件:当节点没有被标记为需要重新布局且约束没有改变时(即 code 3.4 中的!_needsLayout && constraints == _constrains)。经过layout/_layoutWithoutResize的所有node都将标记为markNeedsPaint(),等待之后的Paint流程的处理。
Paint
dart
// code 3.8
bool _needPaint = true;
bool get isRepaintBoundary => false
late bool _wasRepaintBoundary;
void markNeedsPaint();
void paint(PaintingContext context, Offset offset){ }
void _paintWithContext(PaintingContext context, Offset offset) {}
Paint 流程跟 Layout 流程类似,采用的也是mark&&flush的处理,就是先标记再处理。
markNeedsPaint
dart
// code 3.9
void markNeedsPaint() {
...
if (_needsPaint) {
// 已经标记了
return ;
}
_needsPaint = true;
...
// If this was not previously a repaint boundary it will not have
// a layer we can paint from.
if (isRepaintBoundary && _wasRepaintBoundary) {
...
if (owner != null) {
owner!._nodesNeedsPaint.add(this);
owner!.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent! as RenderObject;
parent.markNeedsPaint();
} else {
...
// 只有根 RenderObject 会走到这里
if (owner != null) {
owner!.requestVisualUpdate();
}
}
}
与markNeedsLayout类似,Mark过程是将节点添加到脏列表(在这里是owner!._nodesNeedsPaint),并请求刷新的过程。这里引入了一个新的边界概念,绘制边界isRepaintBoundary,只有绘制边界元素才会添加到脏列表中。
_paintWithContext && paint
dart
// code 3.10
void _paintWithContext(PaintingContext context, Offset offset) {
...
if (_needsLayout) {
return;
}
...
_needsPaint = false;
_needsCompositedLayerUpdate = false;
_wasRePaintBoundary = isRepaintBoundary;
try {
paint(context, offset);
} catch(e, stack) {
_reportException('paint', e, stack);
}
...
}
void paint(PaintingContext context, Offset offset) {
// 具体实现交由子类
}
_paintWithContext跟paint的关系有点像上面提到的layout与performLayout。_paintWithContext会额外处理一些标志位条件和代码的 try catch,paint就只包含真正的绘制代码。那paint究竟怎么处理的呢?
paint
dart
///[RenderClipOval] 中的实现
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (clipBehavior != Clip.none) {
_updateClip();
layer = context.pushClipPath(
needsCompositing,
offset,
_clip,
_getClipPath(_clip!),
super.paint,
clipBehavior: clipBehavior,
oldLayer: layer as ClipPathLayer?,
);
} else {
context.paintChild(child!, offset);
layer = null;
}
} else {
layer = null;
}
}
这里引入了一个新的概念Layer,paint实际上就是对PaintingContext进行各种pushLayer的操作。关于Layer和PaintingContext会在Layer篇中介绍,这里我们只要知道Paint会根据Layout阶段中计算出来的布局信息(在这里是 _clip)执行layer的处理。
到目前为止,我们看到Layout、Paint都有着相似的处理模式。Mark先对标志位做标记,Flush阶段会触发真实的操作,操作又可分成两层,上层即是如layout会在performLayout的基础上多做一层标志位的判断更新和try catch的保护。
那Mark是怎么走向Flush的?
Mark to Flush
无论是markNeedsLayout还是markNeedsPaint,都会调用到owner!.requestVisualUpdate()。跟踪代码会发现,它会经过 SchedulerBinding.ensureVisualUpdate() -> SchedulerBinding.scheduleFrame() -> PlatformDispatcher.scheduleFrame()
dart
/// [SchedulerBinding]
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) {
// 当前帧还在渲染,不允许重复处理
return ;
}
...
ensureFrameCallbacksRegistered();
platformDispatcher.scheduleFrame();
_hasScheduledFrame = true;
}
/// [SchedulerBinding]
void ensureFrameCallbacksRegistered() {
platformDispatcher.onBeginFrame ??= _handleBeginFrame;
platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}
/// [PlatformDispatcher]
void scheduleFrame() => _scheduleFrame();
_scheduleFrame 是请求新一帧的渲染,具体实现在 Engine 中。最终会回调到platformDispatcher.onDrawFrame,而_handleDrawFrame会经过handleDrawFrame的_persistentCallbacks,最终走到[RendererBinding].drawFrame。
dart
/// [RendererBinding]
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compsiteFrame();
pipelineOwner.flushSemantics();
_firstFrameSent = true;
}
}
这个pipelineOwner就是我们前面提到的owner对象,这里我们主要看flushLayout与flushPaint。
dart
void flushLayout() {
// 代码较长,剥离了一些无关的代码
...
try {
while (_nodesNeedingLayout.isNotEmpty) {
...
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
...
for (int i = 0; i < dirtyNode.length; i++) {
...
if (node._needsLayout && node.owner == this) {
node._layoutWithoutResize();
}
}
...
}
} finaly {
...
}
}
void flushPaint() {
...
try {
...
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
...
for(final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
...
if (node._needPaint) {
// 最终会走到 node._paintWithContext 方法
PaintingContext.repaintCompositedChild(node);
} else {
PaintingContext.updateLayerProperties(node);
}
}
} finally {
...
}
}
两个处理都是先对脏列表做深度排序,接着对每一个节点调用相应的处理。对flushLayout来说,最终会走到node._layoutWithResize,对flushPaint来说,最终会走到node._paintWithContext。值得注意的是两者的排序是不一样的,layout是深度从小到大排序,也就是先计算父元素的布局;paint是深度从大到小的排序,先绘制子元素。
总结
到这里,我们基本已经清楚了Layout和Paint两个阶段做的事情,也讲到了Mark和Flush机制。当然了,Layout得到的布局信息是提供给Paint阶段使用的,但Paint中又是怎么根据这些布局信息创建Layer?这就涉及到了 Framework 中渲染最关键的一环:Layer的处理。