Flutter rendering pipeline

First of all, starting from a piece of code (flutter/rendering/binding. Dart) :

void drawFrame(a) { assert(renderView ! = null); pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint();if (sendFramesToEngine) {
      renderView.compositeFrame();  
      pipelineOwner.flushSemantics();
      _firstFrameSent = true; }}Copy the code

This is the flow of the Flutter render pipeline, which is:

graph LR
A[flushLayout] --> B(flushCompositingBits) --> C[flushPaint]

These are the stages that we’re most concerned about right now. Click the method to view the corresponding source code, in fact, are very similar.

flushLayout

Flutter/rendering/Object. Dart# PipeLineOwner:

List<RenderObject> _nodesNeedingLayout = <RenderObject>[]; . (omitted)while (_nodesNeedingLayout.isNotEmpty) {
       final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
       _nodesNeedingLayout = <RenderObject>[];
       for (finalRenderObject node in dirtyNodes.. sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {if (node._needsLayout && node.owner == this) node._layoutWithoutResize(); }}...Copy the code

That is, the RenderObject to be rearranged is added to the List first, and markNeedsLaout is the method to do that, Flutter/rendering/Object. Dart# RenderObject. MarkNeedsLaout (simplified) :

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

If _relayoutBoundary is not the object itself but parent, then markParentNeedsLayout is called and parent.markNeedslayout () is called. This determines which party determines the layout;

_relayoutBoundary

How is _relayoutBoundary determined? In the layout method, it is determined by several condition variables; Flutter/rendering/Object. Dart# RenderObject. Layout (simplified) :

. RenderObject? relayoutBoundary;if(! parentUsesSize || sizedByParent || constraints.isTight || parent is! 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;// Determine _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(); // After the layout is redrawn.Copy the code

Layout optimization is almost all explained here:

  • parentUsesSize

    This parameter is passed by parent when calling child.layout. Default is false. When true, parent can use child.size, otherwise an error will be reported.

  • sizedByParent

  • constraints.isTight

// Whether the constraints are the only input to the sizing algorithm (in
// particular, child nodes have no impact).
//
// Returning false is always correct, but returning true can be more
// efficient when computing the size of this render object because we don't
// need to recompute the size if the constraints don't change.
//
// Typically, subclasses will always return the same value. If the value can
// change, then, when it does change, the subclass should make sure to call
// [markNeedsLayoutForSizedByParentChange].
//
// Subclasses that return true must not change the dimensions of this render
// object in [performLayout]. Instead, that work should be done by
// [performResize] or - for subclasses of [RenderBox] - in
// [RenderBox.computeDryLayout].
@protected
bool get sizedByParent => false;
Copy the code

When you want to redesign the RenderObject, you can override sizeByParent. When the expression returns true, _relayoutBoundary is itself, and size(RenderBox) is fixed unless rearranged, this is because size is set at performResize or computeDryLayout, And you should not set size in performLayout because performLayout is only called when you rearrange.

flushCompositingBits

Example:

context.pushClipRect(needsCompositing, offset, Offset.zero & size,
	defaultPaint, oldLayer: _clipRectLayer);
Copy the code

Call markNeedsCompositingBitsUpdate add renderObject to _nodesNeedingCompositingBitsUpdate, if needsCompositing changes, MarkNeedsPaint will be called; This step is the key to composition, and if we explore it further, we will see that it is all drawn on layer.

flushPaint

Read the source code, markNeedsPaint flutter/rendering/Object. Dart# RenderObject. MarkNeedsPaint (simplified) :

.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(); }...Copy the code

When markNeedsPaint of the renderObject is called, the value of isRepaintBoundary is checked. If it is true, the parent does not have to redraw, marking itself as dirtyNodes and being processed in the render pipe.

Again from flutter/rendering/Object. Dart# PipelineOwner. FlushPaint tracking code to

child._paintWithContext(childContext, Offset.zero);
Copy the code

_paintWithContext internally calls the paint method that’s overridden in the RenderObject. We’re going to say PaintingContext. When we draw a child, we’re going to call context.paintChild;

Flutter/rendering/Object. Dart# PaintingContext. PaintChild:

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

IsRepaintBoundary is true. The child will only be redrawn if child._needspaint is true. This is why other widgets are sometimes wrapped in a RepaintBoundary.