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的处理。

相关推荐
GISer_Jing1 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪2 小时前
CSS复习
前端·css
咖啡の猫4 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲6 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
笔沫拾光7 小时前
iOS 正式包签名指南
flutter·ios·ios签名
朝阳5817 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路7 小时前
GeoTools 读取影像元数据
前端
ssshooter8 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry8 小时前
Jetpack Compose 中的状态
前端
dae bal9 小时前
关于RSA和AES加密
前端·vue.js