RenderObject

RenderObject is a class that determines the location and size of nodes, and handles the relationship between children and parents. When the construction process is complete, a RenderObject tree is generated, and then the layout and drawing phase is entered.

RenderObject contains properties such as parent, parentData, constraints,layout, paint, and other abstract methods. However, there is no specific size and location information such as size and offset in RenderObject. RenderBox is a class that inherits its size from RenderObject. ParentData stores information such as location, which is calculated in real time and passed to Paint by the parent node at draw time. The implementation details will be explained later.

Receive notifications

When does Flutter start the rendering process? Rendering can only be asynchronous in order to make rendering smoother, because you don’t know how many times a developer will render simultaneously in a complex business, rendering itself is expensive, and the interface is bound to stagnate if you go through rendering every time.

If the Future of Flutter is simply rendered asynchronously, there will also be performance issues as there will be multiple asynchronies in the business.

So both Android and iOS have a mechanism called Vsync. Vsync, short for VerticalSynchronization, allows AppUI and SurfaceFlinger to operate at a hardware generated Vsync pace to keep interface refresh and render within 60FPS and visually smooth for humans.

On an article about the construction of the three trees after completion will send a notification, then wait for the arrival of the Vsync signal, in a Flutter, one of SchedulerBinding addPersistentFrameCallback method to register callback listener

/// Register callback listening
void addPersistentFrameCallback(FrameCallback callback) {
  _persistentCallbacks.add(callback);
}
Copy the code

When Vsync is sent to the platform, the window.onDrawFrame method is called and handleDrawFrame is triggered

void handleDrawFrame() {
  assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
  try {
    / / processing addPersistentFrameCallback callback
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
    for (final FrameCallback callback in_persistentCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!) ; _schedulerPhase = SchedulerPhase.postFrameCallbacks;/ / processing addPersistentFrameCallback callback end

    // Handle the callback added by addPostFrameCallback, which will be called the next time the listener is added and only once
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (final FrameCallback callback inlocalPostFrameCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!) ;// Handle the end callback added by addPostFrameCallback
  } finally {
    _schedulerPhase = SchedulerPhase.idle;
    / /...
    _currentFrameTimeStamp = null; }}Copy the code

The above methods can iterate over the callback queue and execution, which also performs by SchedulerBinding. Instance. Add the callback addPostFrameCallback.

While the normal process is to send the window.scheduleFrame event after the tree is built and receive the next debug signal via window.onDrawFrame, the first rendering will make the interface appear faster. In the runApp method, scheduleWarmUpFrame is called first after the build is completed (handleDrawFrame is also called later).

[-> packages/flutter/lib/src/widgets/binding.dart:WidgetsBinding]

void drawFrame() {
  // ...
  try {
    if(renderViewElement ! =null) buildOwner! .buildScope(renderViewElement!) ;super.drawFrame(); buildOwner! .finalizeTree(); }finally {
    / /...
  }
  / /...
}
Copy the code

In application startup, _handlePersistentFrameCallback method will be registered to _persistentCallbacks handleDrawFrame above will trigger the drawFrame method above, it did three things

  1. callBuildOwner.buildScopeTo rebuild the dirty nodes
  2. callsuper.drawFrameMethod to begin the rendering process
  3. callbuildOwner.finalizeTreeDo some global checking in development mode (Key reuse)

Take a look at the rendering flowchart

Its corresponding source code is also very concise

[-> packages/flutter/lib/src/rendering/binding.dart:RenderBinding]

@protected
void drawFrame() {
  assert(renderView ! =null);
  pipelineOwner.flushLayout(); / / layout
  pipelineOwner.flushCompositingBits(); // Update all nodes to calculate the region data to be drawn
  pipelineOwner.flushPaint(); / / to draw
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // Commit the drawing data to the GPU thread
    pipelineOwner.flushSemantics(); // Update semantics to provide UI semantics for some visually impaired people
    _firstFrameSent = true; }}Copy the code

PipelineOwner is a rendering pipeline that controls the layout of a node by holding the RenderObject of the root node and all its children.

layout

Layout is almost an indispensable process of all modern front-end technologies. It determines the specific position of each node through a series of complex calculations, and deals with the parent-child layout relationship and the calculation of display position.

Let’s start with a rough flow chart of the layout

SetSize sets the size of the box

flushLayout

When PipelineOwner calls flushLayout, the layout process is started in flushLayout

[-> packages/flutter/lib/src/rendering/object.dart:PipelineOwner]

void flushLayout() {
  // ...
  try {
    while (_nodesNeedingLayout.isNotEmpty) {
      // Fetch all 'RenderObject' nodes that need to be rearranged
      final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
      _nodesNeedingLayout = <RenderObject>[];
      for (final RenderObject node indirtyNodes.. sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {if (node._needsLayout && node.owner == this)
          // Call the node's _layoutWithoutResize to start the layoutnode._layoutWithoutResize(); }}}finally {
    // ...}}Copy the code

_nodesNeedingLayout is a list that needs to be rebuilt. It stores all nodes that need to be rearranged. During the node building process, markNeedsLayout is used to add itself to the list of nodes to be rearranged. In particular, when the RendererBinding is initialized, the root RenderView is also added to the list ahead of time.

[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]

void _layoutWithoutResize() {
  RenderObject? debugPreviousActiveLayout;
  // ...
  try {
    performLayout();
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    // ...
  }
  _needsLayout = false;
  markNeedsPaint();
}
Copy the code

FlushLayout stores all the nodes that need to be laid out and then calls _layoutWithoutResize for each node. PerformLayout is called in _layoutWithoutResize for the layout.

performLayout

PerformLayout is an abstract method in RenderObject that needs to be implemented by subclasses. RenderProxyBox is the most commonly used subclass of Flutter. Its mixin, RenderProxyBoxMixin, implements performLayout in this way

[-> packages/flutter/lib/src/rendering/proxy_box.dart:RenderProxyBoxMixin]

@override
void performLayout() {
  if(child ! =null) { child! .layout(constraints, parentUsesSize:true); size = child! .size; }else{ size = computeSizeForNoChild(constraints); }}Copy the code

The logic is simple. If the current node has a child node, the layout of the child node is called and size is set to the size of the child node. If no child node exists, the size is assigned by calling computeSizeForNoChild.

Layout, performResize

Layout exists in the RenderObject class and does not participate in the actual layout, so you should generally not override this method either. It is used by the parent node to layout the child node by calling child.layout.

[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]

void layout(Constraints constraints, { bool parentUsesSize = false{})// ...
  RenderObject? relayoutBoundary;
  // Determine the current relayoutBoundary. Normally relayoutBoundary is its own or ancestor node
  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) {
    assert(() {
      _debugDoingThisResize = true;
      return true; } ());try {
      performResize();
      // ...
    } catch (e, stack) {
      _debugReportException('performResize', e, stack); }}// ...
  try {
    performLayout();
    markNeedsSemanticsUpdate();
    assert(() {
      debugAssertDoesMeetConstraints();
      return true; } ()); }catch (e, stack) {
    _debugReportException('performLayout', e, stack);
  }
  // ...
  _needsLayout = false;
  markNeedsPaint();
}
Copy the code

In order to consider the performance of layout, _relayoutBoundary will be used during layout to optimize the performance. It sets _relayoutBoundary to itself by satisfying one of the following four conditions:

  • ParentUsesSize to false, indicates that the layout of the child node does not affect the parent node, and the parent node does not adjust itself according to the size of the child node
  • SizedByParent to true, indicates that if the constraints from the parent to the child remain unchanged, then the child does not recalculate the box size, nor does the layout change of the child’s child affect the size of the child, such as if the child is always full of the parent.
  • Constraints. IsTight to true, indicating that the box size is uniquely determined after the constraints are determined. For example, if the maximum height and minimum height of the box are the same, and the maximum width and minimum width are the same, then the box size is determined
  • The parent is not RenderObjectWhen the parent isAbstractNodeType, so it still existsparentnotRenderObjectFor exampleSemanticsNode(Semantic auxiliary node)

Otherwise the _relayoutBoundary points to the _relayoutBoundary of the parent node. When the current class calls markNeedsLayout, it iterates from the current to the parent until it finds a _relayoutBoundary and marks all iterated nodes as _needsLayout=true. If the _relayoutBoundary node of the current class is as close to you as possible, it should be you.

PerformResize is called when sizedByParent is true, and that size is defined in performResize. It won’t be changed in later performLayout methods. In this case, performLayout is only responsible for layout child nodes.

RenderBox

As mentioned earlier, RenderObject only controls the drawing process. It does not specify the size of the box, which is defined by the RenderBox class

RenderBox has some important properties and methods

  • Size size: Defines the box size
  • BoxConstraints constraints: box constraint, which holds the maximum and minimum height and width limits of the box, passed by the parent box
  • Size computeDryLayout(): this method is defined in Flutter2.0 for whenSizedByParent to trueIs used to calculate the size of the box. You can’t assign size inside the box. You just need to return its calculated size. Unable to calculate box size, returnSize.zero. If we customizeRenderObjectIn the classsizedByParent=true, just need to inherit to implement this method to calculate the layout size.
  • bool hitTest(BoxHitTestResult result,{required Offset position}): A hit test for an event that traverses the current and child nodes and adds the current node to the list of hit test results if an event occurs in the current node.

Determine the location and parentData

Before, the layout stage will determine the box size and position, so how to save the position, the answer is in this parentData.

When the box is initialized, the parent node calls setupParentData to initialize the parentData of the child node

[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]

void setupParentData(covariant RenderObject child) {
  if (child.parentData is! ParentData)
    child.parentData = ParentData();
}
Copy the code

ParentData is just a simple empty method that needs to be inherited to define its own information, such as the most common BoxParentData, which is used to store the location of the child node when we only have one child node

[-> packages/flutter/lib/src/rendering/box.dart:BoxParentData]

class BoxParentData extends ParentData {
  Offset offset = Offset.zero;

  @override
  String toString() => 'offset=$offset';
}
Copy the code

I’ll use an example of the Center component we use a lot to see how location is determined.

The Center component inherits the Align component, which creates the RenderObject through the RenderPositionedBox class

class Center extends Align {
  const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
/ /...
class Align extends SingleChildRenderObjectWidget {
  const Align({
    Key? key,
    this.alignment = Alignment.center,
    this.widthFactor,
    this.heightFactor,
    Widget? child,
  });
  // ...
  @override
  RenderPositionedBox createRenderObject(BuildContext context) {
    return RenderPositionedBox(
      alignment: alignment,
      widthFactor: widthFactor,
      heightFactor: heightFactor,
      textDirection: Directionality.maybeOf(context),
    );
  }
  // ...
}
Copy the code

Align’s constructor gives the alignment property alalignment. Center by default. How does RenderPositionedBox determine the size and position

[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderPositionedBox]

@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  // _widthFactor indicates that the container size is a multiple of the subbox, True if _widthFactor is not empty or if the maximum length of the box is maximum (when constraints. MaxWidth == double. Infinity and _widthFactor is empty, the box width is the actual width of the subbox)
  final boolshrinkWrapWidth = _widthFactor ! =null || constraints.maxWidth == double.infinity;
  final boolshrinkWrapHeight = _heightFactor ! =null || constraints.maxHeight == double.infinity;
  if(child ! =null) {
    // Layout the child nodeschild! .layout(constraints.loosen(), parentUsesSize:true);
    // Set the box sizesize = constraints.constrain(Size(shrinkWrapWidth ? child! .size.width * (_widthFactor ??1.0) : double.infinity, shrinkWrapHeight ? child! .size.height * (_heightFactor ??1.0) : double.infinity));
    // Set the location of the child node
    alignChild();
  } else {
    size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity,
                                      shrinkWrapHeight ? 0.0 : double.infinity)); }}Copy the code

We see that after its performLayout phase ends, it calls alignChild to set the position of the child box

[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderAligningShiftedBox]

@protected
void alignChild() {
  _resolve();
  // ...
  finalBoxParentData childParentData = child! .parentData!asBoxParentData; childParentData.offset = _resolvedAlignment! .alongOffset(size - child! .sizeas Offset);
}
Copy the code

Size = the size of the current box minus the size of the child box. In this case, we just need to divide the length and width of the size by 2 to know the offset of the child box relative to the parent box. The Offset is then assigned to the child’s parentData. The child node’s parentData now stores its position relative to the parent node.

conclusion

HandleDrawFrame is used to start the drawing process of Flutter. PipelineOwner is used in the drawFrame method to carry out the layout, composition, drawing and submission processes. PerformLayout is a method called by each node. Below it we usually set the size of the box and call the layout method of the child node, which also calls performLayout. The layout is quite complex, and the child node will affect the parent node, and the size of the parent node will affect the child node, so sizedByParent, parentUsesSize and other attributes are introduced, and _relayoutBoundary is also introduced to optimize the performance of the layout. ParentData is used to obtain the location information of storage nodes for drawing.

At this stage, our layout process is complete and we are ready to draw. I will keep you updated on what was done and what optimizations were made during the drawing process.