美文网首页
Flutter Framework 渲染流程分析(三):Rend

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

作者: BlueSocks | 来源:发表于2023-08-07 11:51 被阅读0次

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

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

    RenderObject

    首先来看一下RenderObject下的子类

    1.png

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

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

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

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

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

    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()

    // 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的实现。

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

    Layout 总结

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

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

    这里引入了一个新的概念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()

    /// [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对象,这里我们主要看flushLayoutflushPaint

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

    相关文章

      网友评论

          本文标题:Flutter Framework 渲染流程分析(三):Rend

          本文链接:https://www.haomeiwen.com/subject/eargpdtx.html