[fluent core class analysis] deeply understand RenderObject

background

Widget, Element and RenderObject are three core members of the fluent framework. Let's learn RenderObject together in this article.

Macroscopically speaking, a RenderObject is an object in the RenderObject Tree, one of the three trees we mentioned earlier. Its responsibilities mainly include three: layout, rendering and hit test. The hit test is in the previous article This article has an in-depth understanding of the Flutter event mechanism After a detailed analysis, we will mainly explain the other two points: layout and drawing.

RenderObject classification

RenderObject itself is an abstract class, and its concrete implementation is also in the charge of its subclasses. Let's take a look at its classification first:

As shown in the figure, renderobjects are mainly classified into four categories:

  • RenderSliver

    Renderslaver is the base class of all renderobjects that implement sliding effects. Its common subclasses include renderslaversingleboxadapter, etc.

  • RenderBox

    RenderBox is a base class of renderobject using 2D Cartesian coordinate system. General renderobject inherits from RenderBox, such as RenderStack. It is also the base class of general custom renderobject.

  • RenderView

    RenderView is the root node of the entire RenderObject Tree and represents the entire output interface.

  • RenderAbstractViewport

    RenderAbstractViewport is an interface designed for renderobjects that display only part of their contents.

Let's expand from several key nodes in the RenderObjct life cycle: creation, layout and rendering

establish

When it comes to the creation of RenderObject, I believe I have read the previous article: Deep understanding of widgetsDeep understanding of Element When our Element is mounted on the Element tree, we will call the RenderObjectWidget.createRenderObject method to create the RenderObjectElement, and then call the Element.attachRenderObject method to attach it to the RenderObject Tree, that is, the RenderObject Tree will be created step by step during the creation of the Element.

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

drive

To understand the refresh principle of RenderObject, we need to first understand how the Fletter drives the refresh. Let's first look at the figure:

  • When the RenderObject needs to be rearranged, call the markNeedsLayout method, which will add the current RenderObject to the PipelineOwner#_nodesNeedingLayout or pass it to the parent node for processing;
  • When the composition bits of the RenderObject change, call the markNeedsCompositingBitsUpdate method, which will add the current RenderObject to the PipelineOwner#_nodesNeedingCompositingBitsUpdate or pass it to the parent node for processing;
  • When RenderObject needs to repaint, call the markNeedsPaint method, which will add the current RenderObject to the PipelineOwner#_nodesNeedingPaint or pass it to the parent node for processing;
  • When the semantics of the RenderObject changes, call the markNeedsSemanticsUpdate method, which will add the current RenderObject to the PipelineOwner#_nodesNeedingSemantics or pass it to the parent node for processing

The above is the process of PipelineOwner continuously collecting Dirty RenderObjects.

For the above four markNeeds * methods, except markNeedsCompositingBitsUpdate, other methods will finally call pipelineowner #requestvisualupdate. The reason why markNeedsCompositingBitsUpdate will not call PipelineOwner#requestVisualUpdate is that it will not appear alone, but must appear together with one of the other three.

With the pipelineowner #requestvisualupdate - > rendererbinding #scheduleframe - > window #scheduleframe call chain, the information that the UI needs to be refreshed is finally passed to the Engine layer.
Specifically, Window#scheduleFrame mainly requests the Engine to call the Window#onBeginFrame and Window#onDrawFrame methods when the next frame is refreshed.

From the above figure, we can see that the refresh of each frame will call pipelineowner.flushlayout, that is, the layout of all required layout objects.

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

Firstly, the PipelineOwner sorts the collected needed layout renderobjects in ascending order according to their depth on the RenderObject Tree, mainly to avoid repeated layout of child nodes (because the parent node will also recursively layout the child tree when it is laid out);
Secondly, the sequenced and qualified RenderObjects are called in turn_ layoutWithoutResize to perform the layout operation.

Drive summary:

To sum up, first add the renderObject that needs layout to the_ nodesNeedingLayout calls scheduleFrame, and the next frame will be laid out as needed. There are two driving processes for layout:

  • First start

    The first entry to start must be in runApp()

  • UI Layout changes after startup

    The layout driver is the markNeedsLayout shown above

layout

From the above analysis, we know that when RenderObject needs (RE) Layout, it will call the markNeedsLayout method, which will be used by the owner_ Nodesneedinglayout is collected and the Layout operation is triggered at the next frame. Now let's take a look at the calling scenarios of markNeedsLayout.

  • RenderObject is added to the RenderObject tree

    In the attachRenderObject method of RenderObjectElement, the applyParentData method of ParentDataWidget will be called indirectly, and targetParent.markNeedsLayout() will be called in its method

  • Child nodes: adopt, drop, move

    @override
    void adoptChild(RenderObject child) {
      setupParentData(child);
      //here
      markNeedsLayout();
      markNeedsCompositingBitsUpdate();
      markNeedsSemanticsUpdate();
      super.adoptChild(child);
    }
    
    @override
    void dropChild(RenderObject child) {
      child._cleanRelayoutBoundary();
      child.parentData!.detach();
      child.parentData = null;
      super.dropChild(child);
      //here
      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);
      //here
      markNeedsLayout();
    }
    
  • The call is passed by the markNeedsLayout method of the child node

    @protected
    void markParentNeedsLayout() {
      _needsLayout = true;
      final RenderObject parent = this.parent! as RenderObject;
      if (!_doingThisLayoutWithCallback) {
        //here
        parent.markNeedsLayout();
      } else {
      }
    }
    
  • When the layout related attributes of RenderObjectElement change, such as the widthFactor of RenderPositionedBox

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

Of course, the above markNeedsLayout is just a marking process. After marking, the next frame needs layout when it comes.

What are the specific settings for layou? We still need to start with the RenderObject.layout method:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject? relayoutBoundary;
  // relayoutBoundary = this in the following four cases
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } else {
    //The of the parent class_ Relayboundary is assigned to relayboundary
    relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
  }
  // Here you can determine whether layout is required
  // _ needsLayout. If it is true, it will display layout
  // _ constraints are constraint information passed from the parent layout. Layout is required if there is any change
  // _ Relayboundary this is only an optimization measure of the shuttle framework_ needsLayout && constraints == _ If both constraints are true, judge whether it needs to be updated according to this, and focus on the following analysis
  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();
}

A very important concept in renderobject design is relay boundary, which literally means layout boundary. Relay boundary is an important optimization measure to avoid unnecessary re layout. Its main performance is in variables_ On the relayboundary. When a renderobject is a relay boundary, it will cut off the propagation of layout dirty to the parent node, that is, the parent node does not need re layout when the next frame is refreshed. Under what circumstances is this renderobject a relayboundary? The if statement of the above source code also describes it clearly:! parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject

  • parentUsesSize is false

    That is, the parent node will not use the size information of the current node during layout (that is, the layout information of the current node has no impact on the parent node);

  • sizedByParent is true

    That is, the size of the current node is completely determined by the constraints of the parent node, that is, if the constraints passed down in the two layouts are the same, the size of the current node after the two layouts is also the same;

  • constraints.isTight is true

    The effect is the same as that when sizedByParent is true, that is, the layout of the current node will not change its size, which is uniquely determined by constraints;

  • Parent node is not a RenderObject:

    Whose parent node is not RenderObject, just think about it. It is the root node. Its parent node is null

The Render Object with sizedByParent as true needs to override the performResize method, in which the size is calculated only according to constraints. For example, the default behavior of performResize defined in RenderBox: the minimum size under constraints is Size.zero:

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

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

If the parent node layout depends on the size of the child node, set the parentUsesSize parameter to true when calling the layout method.
In this case, if the child node re layout changes its size, the parent node needs to be notified in time, and the parent node also needs re layout (that is, the layout dirty range needs to be propagated upward). All this is achieved through the relay boundary described in the previous section.

In essence, layout is a template method, and the specific layout work is completed by performLayout method. RenderObject#performLayout is an abstract method, and subclasses need to be overridden.

There are several points to note about performLayout:

  • This method is called by the layout method. When re layout is required, the layout method should be called instead of performLayout;
  • If sizedByParent is true, the method should not change the size of the current Render Object (its size is calculated by performResize method);
  • If sizedByParent is false, this method not only performs layout operation, but also calculates the size of the current Render Object;
  • In this method, you need to call the layout method on all its child nodes to perform the layout operation of all child nodes. If the current Render Object depends on the layout information of child nodes, you need to set the parentusesize parameter to true.

Let's take a look at the performLayout of RenderFlow

@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;
  }
}
  • Call the layout method one by one for all child nodes;
  • Calculate the size of the current Render Object;
  • Store the information related to the child node layout in the parent data of the corresponding child node.

draw

Similar to markNeedsLayout, when the Render Object needs to be repainted (paint dirty), it is reported to the PipelineOwner through the markneedsplaint method. In the same way, call owner.requestVisualUpdate(); Drive layout drawing process

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

The internal logic of markNeedsPaint is very similar to markNeedsLayout:

  • If the current Render Object is a retain boundary, it is added to the pipelineowner#_ In nodesneedingpaint, the Paint request also ends;
  • Otherwise, the Paint request propagates to the parent node, that is, the range of re paint needs to be expanded to the parent node (this is a recursive process);
  • There is a special case, that is, the root node of Render Object Tree, RenderView, whose parent node is nil. In this case, you only need to call PipelineOwner#requestVisualUpdate.

PipelineOwner#_ All render objects collected by nodesneedingpaint are repair boundary.

Like the relay boundary mentioned in the layout above, there is also a same design for the relay boundary in painting. According to the above analysis of the relay boundary, if a Render Object is a relay boundary, it will cut off the propagation of re paint request to the parent node.

Straighter and whiter, the repeat boundary enables Render Object to be painted independently of the parent node, otherwise the current Render Object will be painted on the same layer as the parent node. To sum up, repair boundary has the following characteristics:

  • Each repair boundary has its own OffsetLayer (ContainerLayer), and the drawing results of itself and its descendant nodes will be attach ed to the subtree with this layer as the root node;
  • Each repair boundary has its own PaintingContext (including the canvas behind it), so that its drawing is completely separated from the parent node.

The fluent framework predefines the RepaintBoundary widget for developers, which inherits from the SingleChildRenderObjectWidget. When necessary, we can add the repaintboundary through the RepaintBoundary widget.

The above analysis is also a marking process. When it is marked that it needs to be redrawn, the current element will be refreshed when the next frame comes. The specific refresh action should be analyzed according to the RenderObject.paint method:

void paint(PaintingContext context, Offset offset) { }

paint in the abstract base class RenderObject is an empty method and needs to be overridden by subclasses.
The paint method mainly has two tasks:

  • Currently, the paint method of Render Object itself, such as RenderImage, is mainly responsible for image rendering

    @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,
      );
    }
    
  • Draw child nodes, such as RenderTable. The main responsibility of the paint method is to call the PaintingContext#paintChild method to draw each child node in turn:

    @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);
        }
      }
    	//......
    }
    

prose summary

This paper makes a detailed analysis of the layout and drawing driver of the fluent UI. The calling process of the fluent framework can be clearly seen through the flow chart. At the same time, it also focuses on the two concepts of relay boundary and repair boundary of RenderObject.

Tags: Flutter

Posted on Mon, 22 Nov 2021 20:36:28 -0500 by windyweather