This article is the seventh in a series of articles on the Flutter Framework. Several key nodes in the RenderObject lifecycle: creation, layout, and rendering are briefly analyzed.

This post is also published on my personal blog

This series of articles will delve into the Flutter Framework to analyze its core concepts and processes step by step, including:

  • “Simple and Profound Widgets of the Flutter Framework”
  • “Build downer of the Flutter Framework”
  • “Simple elements of the Flutter Framework”
  • “PaintingContext of the Flutter Framework”
  • “Layer of the Flutter Framework”
  • The Depth of Flutter Framework PipelineOwner
  • RenderObejct of the Flutter Framework
  • The Binding of the Flutter Framework
  • “Deep and shallow Rendering Pipeline of Flutter Framework”
  • Custom Widgets for the Flutter Framework

Overview


It can be said that RenderObject belongs to the core object in the whole Flutter Framework. Its responsibilities can be summarized as “Layout”, “Paint” and “Hit Testing”. However, RenderObject is an abstract class, and subclasses do the work.

RenderSliver, RenderBox, RenderView, and RenderAbstractViewport are all subclasses of the Flutter Framework.

  • RenderSliver, “Sliver-widget” corresponds to the Base RenderObject;
  • RenderBox, the Base RenderObject corresponding to almost all common Render- widgets except “sliver-widgets”;
  • RenderViewIs a special Render Object, which is the root node of the RenderObect Tree;
  • RenderAbstractViewport, is mainly used for scroll-widget.

The diagram above Outlines the main properties and methods related to Layout and Paint in RenderObject (which is the main topic of this article). The dashed lines are the methods that the RenderObject subclass needs to override. As mentioned above, RenderObject is abstract and it does not specify which coordinates Cartesian coordinates or Polar coordinates are used, It also does not specify which typesetting algorithm to use (width-in-height-out or constraint-in-size-out).

RenderBox uses a Cartesian coordinate system and the layout algorithm is constraint-in-size-out, that is, size is calculated according to the layout constraints passed by the parent node.

Let’s start with the key nodes in the RenderObject lifecycle: creation, layout, and rendering.

The code shown in this article is based on Flutter 1.12.13 and has been streamlined to highlight the main points discussed.

create


When RenderObjectElement is mounted to the “Element Tree”, a corresponding RenderObject is created. At the same time, it will attach to the “RenderObject Tree”, that is, during the creation of the “Element Tree”, the “RenderObject Tree” is also created:

// RenderObjectElement
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}
Copy the code

layout


The markNeedsLayout method is called when the RenderObject needs to be laid out (again), which is collected by PipelineOwner. The Layout action is triggered when the next frame is refreshed.

MarkNeedsLayout calls the scenario

  • RenderObject added to “RenderObject Tree”;
  • Sub-nodes adopt, drop, and move.
  • By the child nodemarkNeedsLayoutMethod pass calls;
  • The Render Object itself changes its layout-related properties, such asRenderFlexWhen there is a change in typesetting direction:
set direction(Axis value) {
  assert(value ! =null);
  if (_direction != value) {
    _direction = value;
    markNeedsLayout();
  }
}
Copy the code

Relayout Boundary

A Render Object is a “Relayout Boundary” if its layout changes do not affect the layout of its parent. Relayout Boundary is an important optimization measure to avoid unnecessary re-layout.

When a Render Object is Relayout Boundary, it will cut off the layout dirty propagation to the parent node, i.e. the parent node does not need re-layout for the next frame refresh.

As shown above:

  • ifRDA node appears layout dirty due to itself and its parentRA,RRootIt is not Relayout Boundary. The final layout dirty propagates to the root nodeRenderView, causing the entire “RenderObject Tree” to be rearranged;
  • ifRFLayout Dirty appears on the node due to its parentRBSpread dirty to Relayout Boundary, layoutRBThe end, the only thing that needs to be rearrangedRB,RFTwo nodes;
  • ifRGThe node appeared layout dirty, because it was Relayout Boundary, the only thing that needed to be rearranged finally wasRGoneself

So, specifically, what conditions should be met in order to become Relayout Boundary?

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject relayoutBoundary;
  if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
    relayoutBoundary = this;
  } 
  else {
    relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
  }

  if(! _needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {return;
  }

  if(_relayoutBoundary ! =null&& relayoutBoundary ! = _relayoutBoundary) { visitChildren(_cleanChildRelayoutBoundary); } _relayoutBoundary = relayoutBoundary; }Copy the code

The code related to Relayout Boundary in RenderObject#layout is shown above. It can be seen that one of the following 4 conditions can be met to become Relayout Boundary:

  • parentUsesSizeforfalse, that is, the parent node will not use the size information of the current node in layout (that is, the layout information of the current node has no impact on the parent node);
  • sizedByParentfortrueThat is, the size of the current node depends entirely on the constraints of the parent node. If the constraints passed in two layouts are the same, the size of the current node will be the same after both layouts.
  • The constraints passed to the current node are Tight and have the same effect assizedByParentfortrueThat is, the layout of the current node does not change its size, which is uniquely determined by constraints.
  • The parent is not of type RenderObject (mainly for the root; its parent is nil).

Each Render Object has a relayoutBoundary property whose value is equal to either its own or its parent’s relayoutBoundary.

markNeedsLayout

  void markNeedsLayout() {
    if (_needsLayout) {
      return;
    }
    if(_relayoutBoundary ! =this) {
      markParentNeedsLayout();
    } 
    else {
      _needsLayout = true;
      if(owner ! =null) {
        owner._nodesNeedingLayout.add(this); owner.requestVisualUpdate(); }}}Copy the code

As you can see from the above code:

  • If the current Render Object is not Relayout Boundary, then the layout request propagates upwards to the parent node (i.e. the layout scope expands to the parent node, which is a recursive process until Relayout Boundary is encountered).
  • If the current Render Object is Relayout Boundary, the layout request to this node will not propagate to its parent.

All layout dirty nodes are collected using PipelineOwner and processed in batches on the next frame refresh rather than updating dirty layouts in real time to avoid unnecessary duplication of Re-layouts.

layout

  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;
    if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
      relayoutBoundary = this;
    } 
    else {
      relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
    }

    if(! _needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {return;
    }
    _constraints = constraints;
    if(_relayoutBoundary ! =null&& relayoutBoundary ! = _relayoutBoundary) { visitChildren(_cleanChildRelayoutBoundary); } _relayoutBoundary = relayoutBoundary;if (sizedByParent) {
      performResize();
    }

    performLayout();
    markNeedsSemanticsUpdate();
    
    _needsLayout = false;
    markNeedsPaint();
  }
Copy the code

The Layout method is the main entry point to trigger a Render Object to update layout information. Typically, the parent node calls the layout method of the child node to update its overall layout. RenderObject subclasses should not override this method; they can override the performResize or/and performLayout methods as needed.

The current Render Object layout is constrained by constraints from the Layout method parameter constraints.

As shown above, the layout of the Render Object Tree is a depth-first traversal. Layout child nodes take precedence over layout parent nodes. The parent node passes layout constraints to its children, who have to comply with these constraints during layout. As a result of the layout of the child node, the parent node can use the size of the child node when layout.

On lines 19 to 21 of the layout code above, if sizedByParent is true, then performResize is called to calculate the size of the Render Object.

The Render Object that sizedByParent is true needs to override the performResize method, where size is computed only from constraints. The default behavior of performResize, as defined in RenderBox, is to take the minimum size under constraints:

  @override
  void performResize() {
    // default behavior for subclasses that have sizedByParent = true
    size = constraints.smallest;
    assert(size.isFinite);
  }
Copy the code

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 re-layout causes its size to change, 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 upwards). All this is achieved through the Relayout Boundary introduced in the last section.

performLayout

Essentially, layout is a template method, and the performLayout method does the actual layout work. RenderObject#performLayout is an abstract method that subclasses should override.

There are a few things to note about performLayout:

  • The method bylayoutMethod call, which should be called when re-layout is neededlayoutMethod, rather thanperformLayout;
  • ifsizedByParentfortrue, the method should not change the size of the current Render ObjectperformResizeMethod calculation);
  • ifsizedByParentforfalse, the method not only performs layout operation, but also calculates the size of the current Render Object.
  • In this method, all of its children are calledlayoutMethod to perform the layout operation on all child nodes. If the current Render Object depends on the layout information of the child nodesparentUsesSizeParameter is set totrue.
// RenderFlex
void performLayout() {
  RenderBox child = firstChild;
  while(child ! =null) {
    final FlexParentData childParentData = child.parentData;
    BoxConstraints innerConstraints = BoxConstraints(minHeight: constraints.maxHeight, maxHeight: constraints.maxHeight);
    child.layout(innerConstraints, parentUsesSize: true);
    child = childParentData.nextSibling;
  }
  
  size = constraints.constrain(Size(idealSize, crossSize));
  
  child = firstChild;
  while(child ! =null) {
    final FlexParentData childParentData = child.parentData;
    double childCrossPosition = crossSize / 2.0 - _getCrossSize(child) / 2.0; childParentData.offset = Offset(childMainPosition, childCrossPosition); child = childParentData.nextSibling; }}Copy the code

The code snippet above is taken from RenderFlex, and you can see that it does roughly three things:

  • Call all the child nodes one at a timelayoutMethods;
  • Calculate the current Render Object size;
  • Stores information related to the layout of the child node to the corresponding child node’sparentDataIn the.

RenderFlex inherits RenderBox and is the Render Object corresponding to Row and Column.

draw


Similar to markNeedsLayout, when a Render Object needs to be painted dirty, it is reported to PipelineOwner using the markNeedsPaint method.

markNeedsPaint

  void markNeedsPaint() {
    if (isRepaintBoundary) {
      assert(_layer is OffsetLayer);
      if(owner ! =null) {
        owner._nodesNeedingPaint.add(this); owner.requestVisualUpdate(); }}else if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
    } 
    else {
      if(owner ! =null) owner.requestVisualUpdate(); }}Copy the code

MarkNeedsPaint internal logic is very similar to markNeedsLayout:

  • If the current Render Object is Repaint Boundary, add it toPipelineOwner#_nodesNeedingPaintThe Paint Request ends with the Paint Request.
  • Otherwise, the Paint Request propagates to the parent node, requiring re-paint to be scoped to the parent node (a recursive process);
  • There is a special case where the root of the Render Object Tree, the RenderView, whose parent is nil, is calledPipelineOwner#requestVisualUpdateCan.

All Render objects collected by PipelineOwner#_nodesNeedingPaint are Repaint Boundary.

Repaint Boundary

If a Render Object is a Repaint Boundary, it will cut off re-paint Request propagation to its parent.

To be more straightforward, Repaint Boundary allows the Render Object to be drawn independently of the parent node, otherwise the current Render Object would be drawn on the same layer as the parent node. To sum up, Repaint Boundary has the following characteristics:

  • Each Repaint Boundary has an OffsetLayer (ContainerLayer) of its own, and the drawing results of its own and descendant nodes are attached to the subtree with this layer as the root node.
  • Each Repaint Boundary has its own PaintingContext (including the Canvas behind it) so that its drawing is completely separated from the parent node.

As shown in the figure above, since Root/RA/RC/RG/RI is a Repaint Boundary, they all have corresponding OffsetLayer. Meanwhile, each Repaint Boundary has its own PaintingContext, so they all have corresponding PictureLayer, which is used to present specific drawing results. For nodes that are not Repaint Boundary, they will be drawn to the PictureLayer provided by the nearest Repaint Boundary ancestor node.

Repaint Boundary will affect the drawing of sibling nodes. For example, RB and RD are drawn on different PictureLayer due to RC being Repaint Boundary.

In implementation, the “Layer Tree” tends to be more complex than shown above, since each Render Object can autonomously introduce more layers during the drawing process.

The goal of Repaint Boundary is to optimize performance, but it can also be seen from the above discussion that Repaint Boundary increases the complexity of the “Layer Tree”. Therefore, more Repaint Boundary is not always better. Only for scenes that require frequent redrawing, such as video.

Flutter Framework for developers to the predefined RepaintBoundary widget, the inherited from SingleChildRenderObjectWidget, We can add RepaintBoundary via the RepaintBoundary widget if necessary.

Paint

void paint(PaintingContext context, Offset offset) { }
Copy the code

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

  • Render the current Render Object itself, as in:RenderImage, itspaintMethod is responsible for rendering the image
  voidpaint(PaintingContext context, Offset offset) { paintImage( canvas: context.canvas, rect: offset & size, image: _image, ... ) ; }Copy the code
  • Draw child nodes, such as:RenderTable, itspaintThe primary responsibility of the method is to call each child node in turnPaintingContext#paintChildMethod to draw:
  void paint(PaintingContext context, Offset offset) {
    for (int index = 0; index < _children.length; index += 1) {
      final RenderBox child = _children[index];
      if(child ! =null) {
        finalBoxParentData childParentData = child.parentData; context.paintChild(child, childParentData.offset + offset); }}}Copy the code

String together

Let’s string together the whole drawing process, as shown in the figure above:

PipelineOwner#flushPaint

When a new frame starts, the PipelineOwner#flushPaint method is triggered to re-paint all the collected “paint-dirty Render Obejcts” :

void flushPaint() {
    try {
      final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];
      
      // Sort the dirty nodes in reverse order (deepest first).
      for (RenderObject node indirtyNodes.. sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {assert(node._layer ! =null);
        if (node._needsPaint && node.owner == this) { PaintingContext.repaintCompositedChild(node); }}}}Copy the code

PaintingContext#repaintCompositedChild

PaintingContext#repaintCompositedChild is a very important method, Create Layer for “paint-Dirty Render Obejcts” (if not available), prepare context for rendering RenderObject and initiate rendering process:

  static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext childContext,
  }) {
    assert(child.isRepaintBoundary);
    OffsetLayer childLayer = child._layer;
    if (childLayer == null) {
      child._layer = childLayer = OffsetLayer();
    } 
    else {
      assert(childLayer is OffsetLayer);
      childLayer.removeAllChildren();
    }
    
    // In normal drawing, childContext passed as an argument is null
    // Therefore, a new PaintingContext is always created here
    //childContext ?? = PaintingContext(child._layer, child.paintBounds); child._paintWithContext(childContext, Offset.zero); childContext.stopRecordingIfNeeded(); }Copy the code

RenderObject#_paintWithContext

RenderObject#_paintWithContext is a relatively simple logic that basically calls the paint method;

  void _paintWithContext(PaintingContext context, Offset offset) {
    if (_needsLayout)
      return;
    _needsPaint = false;
    paint(context, offset);
  }
Copy the code

Paint method as described in the previous section, the concrete drawing operations are done using the vas#draw** family of methods, and the PaintingContext#paintChild method is called on child nodes (if any);

PaintingContext#paintChild

If the child node is repainted, it needs to be drawn on a separate layer. Otherwise, the _paintWithContext method of the child node is directly called to draw in the current paint context:

  void paintChild(RenderObject child, Offset offset) {
    if (child.isRepaintBoundary) {
      stopRecordingIfNeeded();
      _compositeChild(child, offset);
    } 
    else {
      child._paintWithContext(this, offset); }}Copy the code

The following focuses on the handling of the Repaint Boundary as shown in paintChild, where PaintingContext#stopRecordingIfNeeded is first called to stop the current drawing:

  void stopRecordingIfNeeded() {
    if(! _isRecording)return;
    _currentLayer.picture = _recorder.endRecording();
    _currentLayer = null;
    _recorder = null;
    _canvas = null;
  }
Copy the code

StopRecordingIfNeeded first saves the current drawing result to _currentLayer.picture and then does some context cleaning. Set _currentLayer.picture to null. In fact, _currentLayer was added to the “Layer Tree” in the _startRecording method, where the reference in the PaintingContext is set to null.

  void _startRecording() {
    assert(! _isRecording); _currentLayer = PictureLayer(estimatedBounds); _recorder = ui.PictureRecorder(); _canvas = Canvas(_recorder); _containerLayer.append(_currentLayer); }Copy the code

Since _canvas has been set to null, the _startRecording method will be called the next time it is used:

  Canvas get canvas {
    if (_canvas == null)
      _startRecording();
    return _canvas;
  }
Copy the code

PaintingContext#_compositeChild

In _compositeChild, a new render is made on the child node using the repaintCompositedChild and the result (child._layer) is added to the Layer Tree:

  void _compositeChild(RenderObject child, Offset offset) {
    assert(! _isRecording);assert(child.isRepaintBoundary);
    assert(_canvas == null || _canvas.getSaveCount() == 1);

    repaintCompositedChild(child, debugAlsoPaintedParent: true);

    final OffsetLayer childOffsetLayer = child._layer;
    childOffsetLayer.offset = offset;
    appendLayer(child._layer);
  }
Copy the code

summary

The entire rendering process is actually a deep walk through the RenderObject Tree. The Repaint Boundary is drawn independently of the parent node, so separate ContainerLayer(OffsetLayer) and PaintingContext are required.

run


Here is a brief analysis of how a Flutter App can run.

voidrunApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. scheduleAttachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code

RunApp is the entry point to a Flutter project, initializing a Binding, attaching the root widget, and scheduling the first frame.

As shown above, rendererbinging #initInstances creates the RenderView, the root node of the RenderObject Tree.

The following code prepares the first frame for rendering during RenderView initialization, where the “Layer Tree” root node is also created.

  // RenderView
  //
  void prepareInitialFrame() {
    scheduleInitialLayout();
    scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
  }
  
  Layer _updateMatricesAndCreateNewRootLayer() {
    _rootTransform = configuration.toMatrix();
    final ContainerLayer rootLayer = TransformLayer(transform: _rootTransform);
    rootLayer.attach(this);
    return rootLayer;
  }
Copy the code
  // RenderObject
  //
  void scheduleInitialLayout() {
    _relayoutBoundary = this;
    owner._nodesNeedingLayout.add(this);
  }
  
  void scheduleInitialPaint(ContainerLayer rootLayer) {
    _layer = rootLayer;
    owner._nodesNeedingPaint.add(this);
  }
Copy the code

The RootWidget will be created on the WidgetsFlutterBinding#scheduleAttachRootWidget->WidgetsFlutterBinding#attachRootWidget call chain as shown below: RenderObjectToWidgetAdapter.

In RenderObjectToWidgetAdapter# attachToRenderTree method in the root node of the Element “Tree” RenderObjectToWidgetElement be created. At this point:

  • Root Widget(RenderObjectToWidgetAdapter)
  • The root node of the Element Tree (RenderObjectToWidgetElement)
  • The root node of the RenderObject Tree (RenderView)
  • The root node of the Layer Tree (TransformLayer)

The creation is complete.

  // RenderObjectToWidgetAdapter
  RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
	  owner.lockState(() {
	    element = createElement();
	    element.assignOwner(owner);
	  });
	  
	  owner.buildScope(element, () {
	    element.mount(null.null);
	  });
	  
	  // This is most likely the first time the framework is ready to produce
	  // a frame. Ensure that we are asked for one.
	  SchedulerBinding.instance.ensureVisualUpdate();

    return element;
  }
  
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
Copy the code

On line 9, mount the Root Element. As a result, “Element Tree” was gradually created.

For a more detailed analysis of the “Element Tree” creation process, see “Element of the Flutter Framework”

  // RenderObjectToWidgetAdapter
  void _rebuild() {
    _child = updateChild(_child, widget.child, _rootChildSlot);
  }
Copy the code

Along with the “Element Tree” construction, a “RenderObject Tree” is created.

  // RenderObjectElement
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
Copy the code

summary


This article gives a brief introduction to the key nodes in the RenderObject life cycle: creation, layout, and drawing. Some important concepts such as Relayout Boundary and Repaint Boundary are also analyzed in detail.