相较于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
流程有关的属性和接口
// 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()
// 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()
// 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 })
// 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()
// 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
的实现。
// code 3.6
/// [RenderBox]中的实现
void performResize() {
size = computeDryLayout(constraints);
...
}
computeDryLayout
看子类RenderViewport
的实现
/// [RenderViewport]中的实现
Size computeDryLayout(BoxConstraints constraints) {
...
// 取约束中的最大值
return constraints.biggest;
}
简而言之,performResize
就是通过constraints
计算size
的过程。
void performLayout()
跟performResize
一样,RenderObject
中performLayout
交由子类去实现。为了方便理解,我们取一个实现简单的类RenderConstraintBox
去看。
// 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
// 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
// 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
// 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
///[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()
/// [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
。
/// [RendererBinding]
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compsiteFrame();
pipelineOwner.flushSemantics();
_firstFrameSent = true;
}
}
这个pipelineOwner
就是我们前面提到的owner
对象,这里我们主要看flushLayout
与flushPaint
。
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
的处理。
网友评论