Flutter Framework 渲染流程分析(三):RenderObject

本文是Flutter Framework 渲染流程分析中的第三篇

相较于WidgetElementRenderObjet则要复杂的多。界面的布局(Layout)、绘制(Paint)、语义化(Semantics)都在RenderObject中完成,同时它持有一个布局的核心:LayerLayer Tree组成后构成Scene传到Engine最终输出成界面像素。

由于篇幅限制,本文只关注RenderObjectLayoutPaint流程,Layer会在之后的篇幅详细解析。

RenderObject

首先来看一下RenderObject下的子类

RenderObject继承自AbstractNode,我们在第一篇讲过AbstractNode是树节点的抽象基类。RenderObject下大概可以分为三类:

  1. RenderView:比较特殊,它是RenderObject Tree的根元素,只会有一个实例。
  2. RenderSliver:是所有 Sliver 类型组件的基类。
  3. 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开头的方法:真正的处理,这里有两个不同的操作:resizelayout

下面我们通过源码去了解这些方法。

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;另外一个是在RenderViewscheduleInitialLayout(),也就是根节点中。无论是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属于布局边界元素;

这样那么一个节点可以通过以下几种方式确定为布局边界元素:

  1. 是根节点 RenderView
  2. 这个RenderObject类型就是布局边界:覆写RenderObject中的sizedByParent,返回 true
  3. 布局时由上层确定:调用 layout 时的传参parentUsesSizeconstraints决定

在调用 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的过程。

RenderObjectperformResize是一个抽象方法,没有实现。我们来看子类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一样,RenderObjectperformLayout交由子类去实现。为了方便理解,我们取一个实现简单的类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的布局;否则就根据_additionalConstraintsconstraints计算size

Layout 总结

通过调用layout/_layoutWithoutResize触发performResize/performLayout计算布局信息(比如size、padding、offset等),同时也会调用childlayout去计算底下子孙元素的布局,这一个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) {
    // 具体实现交由子类
}

_paintWithContextpaint的关系有点像上面提到的layoutperformLayout_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;
    }
}

这里引入了一个新的概念Layerpaint实际上就是对PaintingContext进行各种pushLayer的操作。关于LayerPaintingContext会在Layer篇中介绍,这里我们只要知道Paint会根据Layout阶段中计算出来的布局信息(在这里是 _clip)执行layer的处理。

到目前为止,我们看到LayoutPaint都有着相似的处理模式。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对象,这里我们主要看flushLayoutflushPaint

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是深度从大到小的排序,先绘制子元素。

总结

到这里,我们基本已经清楚了LayoutPaint两个阶段做的事情,也讲到了MarkFlush机制。当然了,Layout得到的布局信息是提供给Paint阶段使用的,但Paint中又是怎么根据这些布局信息创建Layer?这就涉及到了 Framework 中渲染最关键的一环:Layer的处理。

相关推荐
low神10 分钟前
前端在网络安全攻击问题上能做什么?
前端·安全·web安全
qbbmnnnnnn1 小时前
【CSS Tricks】如何做一个粒子效果的logo
前端·css
唐家小妹1 小时前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪1 小时前
uni-app环境搭建
前端·uni-app
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_032】5.4 Grid 网格布局的显示网格与隐式网格(上)
前端·css·css3·html5·网格布局·grid布局·css网格布局
洛千陨1 小时前
element-plus弹窗内分页表格保留勾选项
前端·javascript·vue.js
小小19921 小时前
elementui 单元格添加样式的两种方法
前端·javascript·elementui
前端没钱1 小时前
若依Nodejs后台、实现90%以上接口,附体验地址、源码、拓展特色功能
前端·javascript·vue.js·node.js
爱喝水的小鼠2 小时前
AJAX(一)HTTP协议(请求响应报文),AJAX发送请求,请求问题处理
前端·http·ajax
叫我:松哥2 小时前
基于机器学习的癌症数据分析与预测系统实现,有三种算法,bootstrap前端+flask
前端·python·随机森林·机器学习·数据分析·flask·bootstrap