Flutter布局和绘制流程浅析

790 阅读10分钟

我们前面介绍了StatelessWidgetStatefulWidget,它们只是对其他Widget进行组合,不具备自定义绘制的能力。在需要绘制内容的场景下,我们要使用RenderObjectWidget,因为RenderObjectWidget创建的RenderObject负责布局和绘制的功能。

本文将以RenderObject为起点,梳理下Flutter的布局和绘制流程的逻辑。

RenderObject

RenderObject渲染树Render Tree的一个节点,主要负责布局绘制

Flutter设计了重要的三棵树Widget Tree - Element Tree - RenderObject Tree。示例如下: 图片引用来源

RenderObjectWidget实例化的RenderObjectElement会创建 RenderObject, 所有的RenderObject会组成一颗RenderObject Tree

abstract class RenderObject extends AbstractNode implements HitTestTarget {}

RenderObject继承自AbstractNode, AbstractNode是对树的节点的抽象:

class AbstractNode {
    // 1
    int get depth => _depth;
    int _depth = 0;
    void redepthChild(AbstractNode child) {}
    
    // 2
    Object? get owner => _owner;
    Object? _owner;
    
    void attach(covariant Object owner) {
        _owner = owner;
    }
    void detach() {
        _owner = null;
    }
    
    // 3
    AbstractNode? get parent => _parent;
    AbstractNode? _parent;
  
    void adoptChild(covariant AbstractNode child) {
        child._parent = this;
        if (attached)
          child.attach(_owner!);
        redepthChild(child);
    }
    
    void dropChild(covariant AbstractNode child) {
        child._parent = null;
        if (attached)
          child.detach();
    }
}
  • AbstractNode提供了三个属性和几个重要的方法:
  1. 节点深度depth属性和计算节点深度redepthChild()方法;
  2. owner和对应的关联attach()和取消关联detach()方法;
  3. parent父节点;
  4. 挂载子节点adoptChild()和卸载子节点dropChild()方法。
abstract class RenderObject extends AbstractNode implements HitTestTarget {
    // 1
    ParentData? parentData;
    
    // 2
    Constraints _constraints;
    
    // 3
    RenderObject? _relayoutBoundary;
    
    // 众多方法...
}
  • RenderObject自身也有几个重要的属性:
  1. parentData父节点的插槽,父节点的一些信息可以放置在这里面供子节点使用;
  2. _constraints为父节点提供的约束;
  3. _relayoutBoundary是需要重新布局的边界。
  • RenderObject的方法和Android的View非常类似:
功能RenderObjectView
布局performLayout()measure()/measure()
绘制paint()draw()
请求布局markNeedsLayout() requestLayout()
请求绘制markNeedsPaint() invalidate()
父节点/ViewparentgetParent()
添加子节点/ViewadoptChild()addView()
移除子节点/ViewdropChild()removeView()
关联owner/Windowattach()onAttachedToWindow()
取消关联owner/Windowdetach()onDetachedFromWindow()
事件hitTest()onTouch()
屏幕旋转rotate()onConfigurationChanged()
参数parentDatamLayoutParams
  • RenderObject还有一个特点 --- 它定义了布局/绘制协议,但并没定义具体布局/绘制模型

定义了布局/绘制协议就是指继承RenderObject的子类必须要实现一些方法,譬如performLayoutpaint等;没定义具体布局/绘制模型是指没有限定使用什么坐标系,子节点可以有0个、1个还是多个等。

  • Flutter提供了RenderBoxRenderSlive两个子类,他们分别对应简单的2D笛卡尔坐标模型和滚动模型。
RenderObject子类ConstraintsParentData
RenderBoxBoxConstraintsBoxParentData
RenderSliveSliverConstraintsSliverLogicalParentData

一般情况下我们不需要直接使用RenderObject,使用RenderBoxRenderSlive这两个子类就能满足需求。

SchedulerBinding.handleDrawFrame()

我们介绍这个方法是为了介绍每次刷新的工作流程,这样有助于我们更好的理解RenderObject的相关内容。

Flutter启动流程分析那篇文章中,我们提到过window.scheduleFrame()Native platform发起一个刷新视图的请求后,Flutter Engine会在适当的时候调用SchedulerBinding_handleDrawFrame方法。

void handleDrawFrame() {
    try {
      // PERSISTENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (final FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp!);
    } finally {
    }
}

handleDrawFrame中执行了回调函数数组persistentCallbacks中所有的回调函数。其中就包括RendererBinding中的_handlePersistentFrameCallback方法:

<!-- RendererBinding -->
void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
    _scheduleMouseTrackerUpdate();
}

这里的drawFrame方法是调用的父类WidgetsBinding的方法:

<!-- WidgetsBinding -->
void drawFrame() {
    try {
      // 1
      if (renderViewElement != null)
        buildOwner!.buildScope(renderViewElement!);
      // 2
      super.drawFrame();
      // 3
      buildOwner!.finalizeTree();
    } finally {
     
    }
}

此方法代表的含义:

  1. buildOwner!.buildScope(renderViewElement!)执行的是Widgetbuild任务,这其中就包括StatelessWidgetStatefulWidgetRenderObjectWidget
  2. 调用WidgetsBindingdrawFrame方法;
  3. 卸载非激活状态的Element

WidgetsBindingdrawFrame方法中则执行了布局和绘制等操作。

void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    if (sendFramesToEngine) {
      renderView.compositeFrame(); // this sends the bits to the GPU
      pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
      _firstFrameSent = true;
    }
}

flow

上面的buid/layout/paint等都和RenderObject息息相关,我们将在接下来的章节中详细介绍。

Build

接下来我们就来看看buildScope方法触发的RenderObjectWidget的构建过程。

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    // 1
    final Element newChild = newWidget.createElement();
    // 2
    newChild.mount(this, newSlot);
    return newChild;
}

inflateWidget方法主要作用:

  1. 先通过createElement方法根据Widget创建对应的Element
  2. 然后新建的Element调用mount方法,将自己挂载到Element Tree上,位置是父ElementnewSlot这个插槽。

createElement

abstract class RenderObjectWidget extends Widget {
    @factory
    RenderObjectElement createElement();
}

RenderObjectWidgetcreateElement方法是工厂方法,真正的实现方法在子类里面。

RenderObjectWidget的子类对应的Element总结:

分类WidgetElement
根节点RenderObjectToWidgetAdapterRootRenderObjectElement
具有多个子节点MultiChildRenderObjectWidgetMultiChildRenderObjectElement
具有一个子节点点SingleChildRenderObjectWidgetSingleChildRenderObjectElement
叶子节点LeafRenderObjectWidgetLeafRenderObjectElement

代码如下:

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  @override
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
  @override
  SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}

abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
  @override
  MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
  @override
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

  @override
  RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
}

mount

我们来看RenderObjectElementmount方法实现:

void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
}
  1. super.mount的作用主要是记录下parent,slotdepth等值;
  2. widget.createRenderObject创建了一个renderObject
  3. attachRenderObject就是将这个parentData挂载到RenderObject Tree上,并且更新RenderObjectparentData
void attachRenderObject(dynamic newSlot) {
    _slot = newSlot;
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
    final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null)
      _updateParentData(parentDataElement.widget);
}

insertRenderObjectChild

renderObject通过insertRenderObjectChild方法挂载到RenderObject Tree上,那具体的实现是如何实现的呢?

能实现挂载RenderObject的只能是SingleChildRenderObjectElementMultiChildRenderObjectElement。我们分别来看看:

SingleChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    renderObject.child = child;
}

child进行赋值:

set child(ChildType? value) {
    if (_child != null)
      dropChild(_child!);
    _child = value;
    if (_child != null)
      adoptChild(_child!);
}

如果已经有_child先将其从卸载,然后将新的Child挂载上。

void dropChild(RenderObject child) {
    child._cleanRelayoutBoundary();
    child.parentData!.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
}
void adoptChild(RenderObject child) {
    setupParentData(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
}

这两个方法主要是对_childparentData重新赋值,然后通过markNeedsLayoutmarkNeedsCompositingBitsUpdatemarkNeedsSemanticsUpdate标记需要重新布局,需要合成和语义的更新。

MultiChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
    final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;
    renderObject.insert(child, after: slot.value?.renderObject);
}
void insert(ChildType child, { ChildType? after }) {
    adoptChild(child);
    _insertIntoChildList(child, after: after);
}

MultiChildRenderObjectElement中的实现方式类似,只是这次不是简单的赋值,而是将child添加到Render Tree中去,然后进行各种标记。

_insertIntoChildList方法添加的逻辑如下:

  • 依附的兄弟节点为空,插入在第一个子节点;
  • 依附的兄弟节点没有相关联的下一个兄弟节点,插入在兄弟节点队尾;
  • 依附的兄弟节点有相关联的下一个兄弟节点,插入在兄弟节点中间。

inflateWidget递归

由于SingleChildRenderObjectWidgetMultiChildRenderObjectWidget含有子节点,所以需要对子Widget进行构建。

<!-- SingleChildRenderObjectWidget -->
void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _child = updateChild(_child, widget.child, null);
}

Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {
    final Element newChild;
    // ... 省略Widget更新的逻辑
    newChild = inflateWidget(newWidget, newSlot);
    return newChild;
}
<!-- MultiChildRenderObjectElement -->
void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
    Element? previousChild;
    for (int i = 0; i < children.length; i += 1) {
      final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
      children[i] = newChild;
      previousChild = newChild;
    }
    _children = children;
}

这样,接下来的操作就进入了递归流程了,和上面介绍的流程内容一模一样了。

流程示意图:

RenderObjectWidget Build

markNeedsLayout

我们上面看到了,adoptChildadoptChild的方法中都调用了markNeedsLayout的相关内容:

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {

    bool _needsLayout = true;

    RenderObject? _relayoutBoundary;
    
    void markNeedsLayout() {
        // 1
        if (_needsLayout) {
          return;
        }
        // 2
        if (_relayoutBoundary != this) {
          markParentNeedsLayout();
        } else {
          // 3
          _needsLayout = true;
          if (owner != null) {
            owner!._nodesNeedingLayout.add(this);
            owner!.requestVisualUpdate();
          }
        }
    }
}

RenderObject_needsLayout属性,标记是否需要重新布局,还有一个_relayoutBoundary布局边界属性,表示开始重新布局的节点,这样就不需要每次整个渲染树的节点都进行重新布局。

markNeedsLayout代表的含义:

  1. 如果已经标记_needsLayout,直接返回;
  2. 如果_relayoutBoundary布局边界不是自身,让父节点递归调用markNeedsLayout方法;
  3. 如果_relayoutBoundary布局边界是自身,标记_needsLayout, 并将自身加入到PipelineOwner_nodesNeedingLayout列表中,等待PipelineOwner进行重新布局;
  4. 请求PipelineOwner进行更新。

您可能会有疑问_relayoutBoundary是在什么时候赋值的?有两个地方赋值:

  1. 第一次布局的时候,_relayoutBoundary会被标记为RenderView,即自身,然后从根节点进行布局;
void scheduleInitialLayout() {
    _relayoutBoundary = this;
    owner!._nodesNeedingLayout.add(this);
}
  1. layout()方法中RenderObject也会重新标记_relayoutBoundary,一般情况下也是自身。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // ...
    _relayoutBoundary = relayoutBoundary;
}

markNeedsCompositingBitsUpdate

bool _needsCompositingBitsUpdate = false;

void markNeedsCompositingBitsUpdate() {
    if (_needsCompositingBitsUpdate)
      return;
    _needsCompositingBitsUpdate = true;
    if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      if (parent._needsCompositingBitsUpdate)
        return;
      if (!isRepaintBoundary && !parent.isRepaintBoundary) {
        parent.markNeedsCompositingBitsUpdate();
        return;
      }
    }
    if (owner != null)
      owner!._nodesNeedingCompositingBitsUpdate.add(this);
}

RenderObject_needsCompositingBitsUpdate属性,标记是否需要合成。

markNeedsCompositingBitsUpdate的逻辑如下:

  1. 如果已经标记_needsCompositingBitsUpdate,直接返回;
  2. 如果未标记_needsCompositingBitsUpdate先标记,然后标记父节点或者向父类递归调用markNeedsCompositingBitsUpdate直到标记成功为止;
  3. 将自身加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

结果就是将isRepaintBoundary这个节点下的所有节点都标记为_needsCompositingBitsUpdate,然后加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

flushLayout

前面所有的逻辑只能算是buildScope方法触发的Build阶段。接下来我们就进入了Layout阶段了。

<!-- PipelineOwner -->
void flushLayout() {
    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 {
    }
}

PipelineOwnerflushLayout其实很简单,让_nodesNeedingLayout中的所有RenderObject按照广度优先遍历调用_layoutWithoutResize方法。

<!-- RenderObject -->
void _layoutWithoutResize() {
    try {
      performLayout();
    } catch (e, stack) {
    }
    _needsLayout = false;
    markNeedsPaint();
}

我们来看看_ScaffoldLayout中的实现:

void performLayout() {
    size = _getSize(constraints);
    delegate._callPerformLayout(size, firstChild);
}

void _callPerformLayout(Size size, RenderBox? firstChild) {
    performLayout(size);
}

void performLayout(Size size) {
    layoutChild(_ScaffoldSlot.body, bodyConstraints);
    positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
}

Size layoutChild(Object childId, BoxConstraints constraints) {
    child!.layout(constraints, parentUsesSize: true);
    return child.size;
}

void positionChild(Object childId, Offset offset) {
    final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData;
    childParentData.offset = offset;
}

根据一系列调用,生成一个BoxConstraints传递给每个子节点,子节点调用layout() 进行测量和布局。

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // 1
    RenderObject? relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
    }
    
    // 2
    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
      return;
    }
    // 3
    _constraints = constraints;
    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
      visitChildren(_cleanChildRelayoutBoundary);
    }
    _relayoutBoundary = relayoutBoundary;
    
    // 4
    if (sizedByParent) {
      try {
        performResize();
        
      } catch (e, stack) {
      }
      
    }
    try {
      // MultiChildLayoutDelegate --- performLayout & _callPerformLayout & performLayout & child!.layout
      // 5 
      performLayout();
      markNeedsSemanticsUpdate();
      
    } catch (e, stack) {
    }
 
    _needsLayout = false;
    markNeedsPaint();
}
  1. 首先根据!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject的条件进行_relayoutBoundary的计算,一般情况下会指向自身;

parentUsesSize表示是否父节点的大小依赖子节点,sizedByParent表示大小由父类决定,constraints.isTight表示大小是固定的。

  1. 根据!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary确实定是否需要重新布局,不需要直接返回;
  2. 记录下_constraints;
  3. 如果依赖父节点的大小,则根据_constraints计算出size尺寸, ;
  4. performLayout根据根据_constraints计算出size尺寸,然后调用子类的layout方法。
总结:

performLayout的逻辑就是通过layout方法将Constraints逐步往下传递,得到Size逐步向上传递,然后父节点通过给parentData赋值确定对子节点的位置摆放。

flushLayout

flushCompositingBits

void flushCompositingBits() {

    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate && node.owner == this)
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
  }

遍历_nodesNeedingCompositingBitsUpdate中的每个RenderObject,然后调用_updateCompositingBits方法。

void _updateCompositingBits() {
    if (!_needsCompositingBitsUpdate)
      return;
    final bool oldNeedsCompositing = _needsCompositing;
    _needsCompositing = false;
    visitChildren((RenderObject child) {
      child._updateCompositingBits();
      if (child.needsCompositing)
        _needsCompositing = true;
    });
    if (isRepaintBoundary || alwaysNeedsCompositing)
      _needsCompositing = true;
    if (oldNeedsCompositing != _needsCompositing)
      markNeedsPaint();
    _needsCompositingBitsUpdate = false;
}

这个方法就是找到isRepaintBoundarytrue的节点及其父节点,将它们的_needsCompositing为true设置为true;

isRepaintBoundary

上面提到的isRepaintBoundaryRenderObject的一个属性,默认是false。表示的是否需要独立渲染。

<!-- RenderObject -->
bool get isRepaintBoundary => false;

如果需要独立渲染则需要覆盖这个值为true,例如RenderView的值就为true

<!-- RenderView -->
@override
bool get isRepaintBoundary => true;

flushPaint

按照逻辑flushPaint之前应该先会调用markNeedsPaint,我们回过头来看看发现确实如此,有很多地方都频繁的调用的markNeedsPaint,譬如_layoutWithoutResize,layout,_updateCompositingBits等方法中都有出现,只是前面我们特意忽略了这个逻辑。

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();
    }
}
  1. 如果isRepaintBoundarytrue, 则加入到_nodesNeedingPaint数组中,然后请求界面更新;
  2. 如果isRepaintBoundaryfalse,则向父节点遍历;
  3. 如果到了根节点,就直接请求界面更新;

我们接下来看看flushPaint的代码:

void flushPaint() {
    try {
      final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];
      for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
        if (node._needsPaint && node.owner == this) {
          if (node._layer!.attached) {
            PaintingContext.repaintCompositedChild(node);
          } else {
            node._skippedPaintingOnLayer();
          }
        }
      }
    } finally {
    }
}

从下往上遍历_nodesNeedingPaint数组,然后从上往下进行绘制。

接下来我们看看是如何绘制的:

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
    _repaintCompositedChild(
      child,
      debugAlsoPaintedParent: debugAlsoPaintedParent,
    );
}

static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext? childContext,
  }) {
    OffsetLayer? childLayer = child._layer as OffsetLayer?;
    if (childLayer == null) {
      child._layer = childLayer = OffsetLayer();
    } else {
      childLayer.removeAllChildren();
    }
    childContext ??= PaintingContext(child._layer!, child.paintBounds);
    // 重点
    child._paintWithContext(childContext, Offset.zero);

    childContext.stopRecordingIfNeeded();
}

PaintingContext的类方法repaintCompositedChild接收了RenderObject对象,最后结果是这个RenderObject对象调用_paintWithContext方法,参数是PaintingContext对象和偏移量Offset

void _paintWithContext(PaintingContext context, Offset offset) {
    
    if (_needsLayout)
      return;

    _needsPaint = false;
    try {
      paint(context, offset);
    } catch (e, stack) {
      
    }
}
<!-- PaintingContext -->
Canvas? _canvas;

_paintWithContext方法调用的是RenderObject子类对象的paint(PaintingContext context, Offset offset)进行绘制,绘制在PaintingContextCanvas上。

总结

本文主要分析了RendObjectBuildLayoutPaint等相关内容,后续继续分析其他相关内容。