其他分享
首页 > 其他分享> > 【Flutter核心类分析】深入理解RenderObject

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

作者:互联网

文章目录

背景

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

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


RenderObject分类

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

RenderObject分类

如图所述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

上述就是 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的驱动流程分为两个:


布局

从上面分析我们知道,当RenderObject需要(重新)布局的时候会调用markNeedsLayout方法,从而被Owner._nodesNeedingLayout收集,并且在下一帧的时候触发Layout操作,那么现在我们来看看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

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有几点需要注意:

下面看看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;
  }
}

绘制

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都非常相似:

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 有以下特点:

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

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

void paint(PaintingContext context, Offset offset) { }

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

文章小结

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

标签:RenderObject,调用,layout,节点,深入,child,Flutter,constraints
来源: https://blog.csdn.net/huangbiao86/article/details/121482190