C/C++教程

【Flutter核心类分析】深入理解RenderObject

本文主要是介绍【Flutter核心类分析】深入理解RenderObject,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

    • 背景
    • RenderObject分类
    • 创建
    • 驱动
      • 驱动小结:
    • 布局
    • 绘制
    • 文章小结

背景

WidgetElementRenderObject可以说是Flutter Framework的三个核心成员,本文我们一起来学习下RenderObject

宏观上来讲,一个RenderObject就是我们之前说的三棵树之一RenderObject Tree中的一个对象,它的职责主要有三个:布局,绘制,命中测试。其中命中测试在之前的文章一文深入了解Flutter事件机制有过详细的分析,本文我们主要来讲解其它两点:布局和绘制。


RenderObject分类

RenderObject本身是一个抽象类,其具体实现也是由其子类负责,我们先来看看它的分类:

RenderObject分类

如图所述RenderObject主要分类四类:

  • RenderSliver

    RenderSliver是所有实现了滑动效果的RenderObject基类,其常用子类有RenderSliverSingleBoxAdapter等。

  • RenderBox

    RenderBox是一个采用2D笛卡尔坐标系的RenderObject的基类,一般的RenderOBject都是继承自RenderBox,例如RenderStack等,它也是一般自定义RenderObject的基类。

  • RenderView

    RenderView是整个RenderObject Tree的根节点,代表了整个输出界面。

  • RenderAbstractViewport

    RenderAbstractViewport是一类接口,此类接口为只展示其部分内容的RenderObject设计。

下面我们从RenderObjct生命周期的几个关键节点展开:创建,布局,渲染


创建

说道RenderObject的创建,相信看过之前文章:深入理解Widget,深入理解Element,都不会陌生,也就是当我们的Element被挂载(mount)到Element tree上时,会调用RenderObjectWidget.createRenderObject方法,创建RenderObjectElement,再调用Element.attachRenderObject方法,将其attach到RenderObject Tree上,也就是在Element的创建过程中RenderObject Tree被逐步创建出来。

@override
void mount(Element? parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}

驱动

想要了解RenderObject的刷新原理,我们需要首先了解下Flutter是如何驱动刷新的,我们首先来看图:

RenderObject_PipelineOwner_RendererBinding

  • 当 RenderObject 需要重新 layout 时,调用markNeedsLayout方法,该方法会将当前 RenderObject 加入 PipelineOwner#_nodesNeedingLayout或传给父节点去处理;
  • 当 RenderObject 的 Compositing Bits 有变化时,调用markNeedsCompositingBitsUpdate方法,该方法会将当前 RenderObject 加入 PipelineOwner#_nodesNeedingCompositingBitsUpdate或传给父节点去处理;
  • 当 RenderObject 需要重新 paint 时,调用markNeedsPaint方法,该方法会将当前 RenderObject 加入PipelineOwner#_nodesNeedingPaint或传给父节点处理;
  • 当 RenderObject 的辅助信息(Semantics)有变化时,调用markNeedsSemanticsUpdate方法,该方法会将当前 RenderObject 加入 PipelineOwner#_nodesNeedingSemantics或传给父节点去处理

上述就是 PipelineOwner 不断收集Dirty RenderObjects的过程。

上述4个markNeeds*方法,除了markNeedsCompositingBitsUpdate,其他方法最后都会调用PipelineOwner#requestVisualUpdate。之所以markNeedsCompositingBitsUpdate不会调用PipelineOwner#requestVisualUpdate,是因为其不会单独出现,一定是伴随其他3个之一一起出现的。

随着PipelineOwner#requestVisualUpdate->RendererBinding#scheduleFrame->Window#scheduleFrame调用链,UI 需要刷新的信息最终传递到了 Engine 层。
具体讲,Window#scheduleFrame主要是向 Engine 请求在下一帧刷新时调用Window#onBeginFrame以及Window#onDrawFrame方法。

从上面的图,我们看到每一帧的刷新都会调用到PipelineOwnder.flushLayout,也就是对所有需要Layou对象的布局。

void flushLayout() {
  if (!kReleaseMode) {
    Timeline.startSync('Layout', arguments: timelineArgumentsIndicatingLandmarkEvent);
  }
  try {
    while (_nodesNeedingLayout.isNotEmpty) {
      final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
      _nodesNeedingLayout = <RenderObject>[];
      for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
        if (node._needsLayout && node.owner == this)
          node._layoutWithoutResize();
      }
    }
  } finally {
    if (!kReleaseMode) {
      Timeline.finishSync();
    }
  }
}

首先,PipelineOwner对于收集到的 Needing Layout RenderObjects 按其在RenderObject Tree上的深度升序排序,主要是为了避免子节点重复 Layout (因为父节点 layout 时,也会递归地对子树进行 layout);
其次,对排好序的且满足条件的 RenderObjects 依次调用_layoutWithoutResize来执行 layout 操作。


驱动小结:

综合来说,就是先将需要layout的renderObject添加到_nodesNeedingLayout在调用scheduleFrame,下一帧来临时就会根据需要layout。layout的驱动流程分为两个:

  • 第一次启动

    第一次启动的入口肯定是在runApp()

    第一次Layout

  • 启动之后UI变更Layout

    之后的layout驱动就是上图的markNeedsLayout

    主动Layout


布局

从上面分析我们知道,当RenderObject需要(重新)布局的时候会调用markNeedsLayout方法,从而被Owner._nodesNeedingLayout收集,并且在下一帧的时候触发Layout操作,那么现在我们来看看markNeedsLayout有哪些调用场景。

  • RenderObject被添加到RenderObject tree

    在RenderObjectElement中的attachRenderObject方法内部会间接调用到ParentDataWidget的applyParentData方法,在其方法内部会调用targetParent.markNeedsLayout()

  • 子节点adopt、drop、move

    @override
    void adoptChild(RenderObject child) {
      setupParentData(child);
      //这里
      markNeedsLayout();
      markNeedsCompositingBitsUpdate();
      markNeedsSemanticsUpdate();
      super.adoptChild(child);
    }
    
    @override
    void dropChild(RenderObject child) {
      child._cleanRelayoutBoundary();
      child.parentData!.detach();
      child.parentData = null;
      super.dropChild(child);
      //这里
      markNeedsLayout();
      markNeedsCompositingBitsUpdate();
      markNeedsSemanticsUpdate();
    }
    
    void move(ChildType child, { ChildType? after }) {
      final ParentDataType childParentData = child.parentData! as ParentDataType;
      if (childParentData.previousSibling == after)
        return;
      _removeFromChildList(child);
      _insertIntoChildList(child, after: after);
      //这里
      markNeedsLayout();
    }
    
  • 由子节点的markNeedsLayout方法传递调用

    @protected
    void markParentNeedsLayout() {
      _needsLayout = true;
      final RenderObject parent = this.parent! as RenderObject;
      if (!_doingThisLayoutWithCallback) {
        //这里
        parent.markNeedsLayout();
      } else {
      }
    }
    
  • RenderObjectElement自身与布局相关属性发生变化的时候,如:RenderPositionedBoxwidthFactor

    double? get widthFactor => _widthFactor;
    double? _widthFactor;
    set widthFactor(double? value) {
      assert(value == null || value >= 0.0);
      if (_widthFactor == value)
        return;
      _widthFactor = value;
      //这里
      markNeedsLayout();
    }
    

当然上面markNeedsLayout只是一个标记过程,标记之后,下一帧来临时候需要layout。

那么layou具体做些设么呢,我们还是需要从RenderObject.layout方法着手:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject? relayoutBoundary;
  // 下列四种情况下relayoutBoundary = this
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } else {
    //将父类的_relayoutBoundary赋值给relayoutBoundary
    relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
  }
  // 这里判断是否需要layout
  // _needsLayout,如果是true,就会layout
  // _constraints 是从父布局传递过来的约束信息,如果有变化的话就需要layout
  // _relayoutBoundary 这只是flutter framework的优化措施如果!_needsLayout && constraints == _constraints两个都成立的话根据这个判断是否需要更新,下面重点分析
  if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
    return;
  }
  _constraints = constraints;
  if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
    visitChildren(_cleanChildRelayoutBoundary);
  }
  _relayoutBoundary = relayoutBoundary;
  if (sizedByParent) {
    try {
      performResize();
    } catch (e, stack) {
      _debugReportException('performResize', e, stack);
    }
  }
  RenderObject? debugPreviousActiveLayout;
  try {
    performLayout();
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    _debugReportException('performLayout', e, stack);
  }
  _needsLayout = false;
  markNeedsPaint();
}

RenderObject设计有一个很重要的概念就是Relayout Boundary,字面意思就是布局边界意思。Relayout Boundary 是一项重要的优化措施,可以避免不必要的 re-layout。它的主要表现就是在变量_relayoutBoundary上。当某个 RenderObject 是 Relayout Boundary 时,会切断 layout dirty 向父节点传播,即下一帧刷新时父节点无需 re-layout。那么什么情况下这个RenderObject就是RelayoutBoundary呢,上面源码的if语句中也描述了很清晰:!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject

  • parentUsesSize为false

    即父节点在 layout 时不会使用当前节点的 size 信息(也就是当前节点的排版信息对父节点无影响);

  • sizedByParent为true

    即当前节点的 size 完全由父节点的 constraints 决定,即若在两次 layout 中传递下来的 constraints 相同,则两次 layout 后当前节点的 size 也相同;

  • constraints.isTight为true

    其效果与sizedByParenttrue是一样的,即当前节点的 layout 不会改变其 size,size 由 constraints 唯一确定;

  • 父节点不是 RenderObject:

    谁的父节点不是RenderObject,想想就知道就是根节点了,它的父节点为null

sizedByParenttrue的 Render Object 需重写performResize方法,在该方法中仅根据constraints来计算 size。如RenderBox中定义的performResize的默认行为:取constraints约束下的最小 size就是Size.zero:

 @override
 void performResize() {
   size = computeDryLayout(constraints);
 }

 @protected
  Size computeDryLayout(BoxConstraints constraints) {
    return Size.zero;
  }

若父节点 layout 依赖子节点的 size,在调用layout方法时需将parentUsesSize参数设为true
因为,在这种情况下若子节点 re-layout 导致其 size 发生变化,需要及时通知父节点,父节点也需要 re-layout (即 layout dirty 范围需要向上传播)。这一切都是通过上节介绍过的 Relayout Boundary 来实现。

本质上,layout是一个模板方法,具体的布局工作由performLayout方法完成。RenderObject#performLayout是一个抽象方法,子类需重写。

关于performLayout有几点需要注意:

  • 该方法由layout方法调用,在需要 re-layout 时应调用layout方法,而不是performLayout
  • sizedByParenttrue,则该方法不应改变当前 Render Object 的 size ( 其 size 由performResize方法计算);
  • sizedByParentfalse,则该方法不仅要执行 layout 操作,还要计算当前 Render Object 的 size;
  • 在该方法中,需对其所有子节点调用layout方法以执行所有子节点的 layout 操作,如果当前 Render Object 依赖子节点的布局信息,需将parentUsesSize参数设为true

下面看看RenderFlow的performLayout

@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  size = _getSize(constraints);
  int i = 0;
  _randomAccessChildren.clear();
  RenderBox? child = firstChild;
  while (child != null) {
    _randomAccessChildren.add(child);
    final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);
    child.layout(innerConstraints, parentUsesSize: true);
    final FlowParentData childParentData = child.parentData! as FlowParentData;
    childParentData.offset = Offset.zero;
    child = childParentData.nextSibling;
    i += 1;
  }
}
  • 对所有子节点逐个调用layout方法;
  • 计算当前 Render Object 的 size;
  • 将与子节点布局有关的信息存储到相应子节点的parentData中。

绘制

markNeedsLayout相似,当 Render Object 需要重新绘制 (paint dirty) 时通过markNeedsPaint方法上报给PipelineOwner。在同样调用owner.requestVisualUpdate();驱动布局绘制流程

void markNeedsPaint() {
  if (_needsPaint)
    return;
  _needsPaint = true;
  if (isRepaintBoundary) {
    if (owner != null) {
      owner!._nodesNeedingPaint.add(this);
      owner!.requestVisualUpdate();
    }
  } else if (parent is RenderObject) {
    final RenderObject parent = this.parent! as RenderObject;
    parent.markNeedsPaint();
  } else {
    if (owner != null)
      owner!.requestVisualUpdate();
  }
}

markNeedsPaint内部逻辑与markNeedsLayout都非常相似:

  • 若当前 Render Object 是 Repaint Boundary,则将其添加到PipelineOwner#_nodesNeedingPaint中,Paint request 也随之结束;
  • 否则,Paint request 向父节点传播,即需要 re-paint 的范围扩大到父节点(这是一个递归的过程);
  • 有一个特例,那就是『 Render Object Tree 』的根节点,即 RenderView,它的父节点为 nil,此时只需调用PipelineOwner#requestVisualUpdate即可。

PipelineOwner#_nodesNeedingPaint收集的所有 Render Object 都是 Repaint Boundary。

跟上面布局中提到的Relayout Boundary一样,绘制中也有一个同样的设计Repaint Boundary,根据上面对Relayout Boundary分析知道若某 Render Object 是 Repaint Boundary,其会切断 re-Paint request 向父节点传播。

更直白点,Repaint Boundary 使得 Render Object 可以独立于父节点进行绘制,否则当前 Render Object 会与父节点绘制在同一个 layer 上。总结一下,Repaint Boundary 有以下特点:

  • 每个 Repaint Boundary 都有一个独属于自己的 OffsetLayer (ContainerLayer),其自身及子孙节点的绘制结果都将 attach 到以该 layer 为根节点的子树上;
  • 每个 Repaint Boundary 都有一个独属于自己的 PaintingContext (包括背后的 Canvas),从而使得其绘制与父节点完全隔离开。

Flutter Framework 为开发者预定义了RepaintBoundary widget,其继承自SingleChildRenderObjectWidget,在有需要时我们可以通过RepaintBoundary widget 来添加 Repaint Boundary。

上面的分析也都是一个标记的过程,标记为需要重新绘制的时候等下一帧来临时候就会重新刷新当前元素。具体的刷新动作我们还是要根据RenderObject.paint方法来分析:

void paint(PaintingContext context, Offset offset) { }

抽象基类RenderObject中的paint是个空方法,需要子类重写。
paint方法主要有2项任务:

  • 当前 Render Object 本身的绘制,如:RenderImage,其paint方法主要职责就是 image 的渲染

    @override
    void paint(PaintingContext context, Offset offset) {
      if (_image == null)
        return;
      _resolve();
      assert(_resolvedAlignment != null);
      assert(_flipHorizontally != null);
      paintImage(
        canvas: context.canvas,
        rect: offset & size,
        image: _image!,
        debugImageLabel: debugImageLabel,
        scale: _scale,
        colorFilter: _colorFilter,
        fit: _fit,
        alignment: _resolvedAlignment!,
        centerSlice: _centerSlice,
        repeat: _repeat,
        flipHorizontally: _flipHorizontally!,
        invertColors: invertColors,
        filterQuality: _filterQuality,
        isAntiAlias: _isAntiAlias,
      );
    }
    
  • 绘制子节点,如:RenderTable,其paint方法主要职责是依次对每个子节点调用PaintingContext#paintChild方法进行绘制:

    @override
    void paint(PaintingContext context, Offset offset) {
      //......
      for (int index = 0; index < _children.length; index += 1) {
        final RenderBox? child = _children[index];
        if (child != null) {
          final BoxParentData childParentData = child.parentData! as BoxParentData;
          context.paintChild(child, childParentData.offset + offset);
        }
      }
    	//......
    }
    

文章小结

本文对Flutter UI的布局,绘制驱动做了详细的分析,通过流程图能够很清晰的看到Flutter framework的调用流程,同时还对RenderObject的Relayout Boundary、Repaint Boundary 这两个概念做了重点分析。

这篇关于【Flutter核心类分析】深入理解RenderObject的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!