How exactly are Widget, Element, and Render trees formed?

Study the most avoid blind, no plan, fragmentary knowledge can not be strung into a system. Where did you learn, where did you forget? I can’t remember the interview. Here I have collated the most frequently asked questions in the interview and several core pieces of knowledge in the Flutter framework, which are analyzed in about 20 articles. Welcome to pay attention and make progress together. ! [Flutter framework]

Guide language:

Recently, after scouring all the articles on the Internet for the performance optimization related to Flutter, I saw the High performance, multi-functional full scene rolling container of The Flutter by Idler Fish. However, this component is not open source, so I’m going to try to research and develop a high performance ScrollView from the ideas presented in this article. This series is expected to be divided into 4-5 articles, with the first three focusing on research and analysis of existing problems and the last two on actual development.

Principle of the article:

1. How do Widget, Element and Render trees form?

2. Analysis of performance problems in ListView

In the first part of the introduction to principles, I want to analyze with you a well-worn topic: the three trees in Flutter. The reason why I chose this topic is that the UI system of Flutter is composed of these three trees. Mastering the construction process of these three trees can give us a clear understanding of the rendering process of Flutter, so that we can analyze the optimization points based on this.

After reading this article, you will get a step-by-step understanding of how the three trees in Flutter are formed from the source code.


Quote:

Trees are one of the structures that must be learned when learning data structures, and Flutter uses this data structure as the core of the UI architecture. If you have any experience with Flutter development you must have been exposed to this concept, we can see a similar introduction in various articles (photo from the article)The three tree rendering mechanism and principle of FlutterError structure ××××!!!!)And when we look at this picture we probably assume that we understand this by default, but do these trees really look like this? How on earth did they form such structures? This article will trace the source code through a simple example to reveal the process.

This article is based on a minimalist demoContainer has a green background. A Cloumn has four child nodes.


1, the Widget tree

First let’s take a look at the Widget tree. There is actually no clear concept of a Widget tree when the code is running. This tree is more of a description of the nested structure of the Flutter during development. And based on the code we can think of the Widget tree as looking like this


2. The building process of the Element tree

The Element tree is arguably the most important structure, bridging the Widget we are developing with the RenderObject object we are actually rendering. Based on most articles on the Web, you might think that the Element tree in this example is the same as the Widget treeIf you think so, then congratulations, you are wrong. What should the tree look like

After summarizing 30 examples, I realized that, as mentioned in the layout principle of Flutter, the widgets in the UI system of Flutter can be roughly divided into three categories: combination (purple logo), agent (red logo), and drawing (yellow logo).

StatelessWidget and StatefulWidget belong to the composite Widget class, which is not actually responsible for drawing, and has multiple layers of widgets nested inside. Almost all of the UIs we see on screen end up with widgets that draw classesRenderObjectWidgetThe implementation. There’s a RenderObjectWidgetcreateRenderObject()Method to generate the actual renderedRenderObjectObject. In our example, Container and Text areWidget for combining classesWe can sort out the nesting of widgets by looking at their build methods. In our example,ContainerandTextActually all isCombination type of Widget.Looking at the Build method of the Container, we know from the source that the level of nesting of the Container is related to the properties we set. We set the child and color properties for the Container, which will nest our child with a DecoratedBox (rendering class Widget). The Text component internally just returns oneRichText(Render class Widget). You get the following structure (see, the leaf node must be a render Widget, because it will be rendered to the screen)Would our actual Element tree look like this? That’s getting close to the truth. This is not quite accurate because there is no Element of type Container, so let’s take a look at what type the Element object actually has.ComponentElement refers to widgets (Stateless/Stateful and all their subclasses Container, Text) that were ComponentElement, ProxyElement refers to proxy widgets that were Stateless/Stateful and all their subclasses Container, Text) that were ComponentElement. RenderObjectElement corresponds to the Widget of the render class (e.g. RichText, Column). So a more accurate Element tree structure would look like this:Why is there still a missing, see the following analysis:

How did the tree come into being? The core methods are in their common ancestor, the element.mount () method (the code has tried to omit extranet logic)

  /// Add this element to the tree in the given slot of the given parent.
  ///
  /// The framework calls this function when a newly created element is added to
  /// the tree for the first time. Use this method to initialize state that
  /// depends on having a parent. State that is independent of the parent can
  /// more easily be initialized in the constructor.
  ///
  /// This method transitions the element from the "initial" lifecycle state to
  /// the "active" lifecycle state.
  @mustCallSuper
  void mount(Element parent, dynamicnewSlot) { _parent = parent; _slot = newSlot; _depth = _parent ! =null ? _parent.depth + 1 : 1;
    _active = true;
    if(parent ! =null) // Only assign ownership if the parent is non-null
      _owner = parent.owner;
    _updateInheritance();
  }
Copy the code

The comment above tells us that the function of this method is to add the current Element object to the solt position of the parent node in the tree. Method, we see that he sets the current Element object’s _parent = parent, so we’re actually referring the child node’s _parent property to the parent node with a depth of +1. Mount () was overridden to the Widget: Stateful/Stateless, and performRebuild() was called to the method ejbateful /Stateless

  @override
  void performRebuild() {
    Widget built;
    built = build();
    _child = updateChild(_child, built, slot);
  }
Copy the code

If the tree is built for the first time, child nodes will be generated using inflateWidget logic, and the child nodes will be inserted into the tree by calling the mount of the child nodes (since there are multiple layers nested in the Widget of the composite class, the build will only return a child, which will be inserted into the tree).

  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Element newChild = newWidget.createElement();
    // Insert child nodes into the tree
    newChild.mount(this, newSlot);
    return newChild;
  }
Copy the code

RenderObjectElement is similar, which we’ll parse below.

So the more accurate Element tree here is as followsWell, there are just more references between nodes, and the middle layer is different from before. (See Cloumn mount())

3, RenderObejct tree building process

So what does RenderObjectTree look like? Just as an aside, at first you might get confused about the relationship between RenderObjectWidget and RenderObject, RenderObjectElement. RenderObjectWidget is a Widget!! Such as SizeBox, Cloumn, etc. RenderObjectElement is such widgets generated Element types, such as the corresponding SingleChildRenderObjectElement SizeBox Element node (list), RenderObject is the object that is really responsible for drawing, which includes methods such as Paint, layout and so on

Then analysis, and we just said the whole tree core logic on the mount () here, so the view RenderObjectElement. Mount ()

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

After executing super.mount(parent, newSlot), the mount() process above, attachRenderObject(newSlot)

@override
  void attachRenderObject(dynamic newSlot) {
    _slot = newSlot;
    // Query the most recent RenderObject
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    // Insert the renderObject object of the current node above below RenderObjecte_ancestorRenderObjectElement? .insertChildRenderObject( renderObject, newSlot);/ /... /
  }
Copy the code

Which findAncestorRenderObjectElement ()

 RenderObjectElement _findAncestorRenderObjectElement() {
    Element ancestor = _parent;
    while(ancestor ! =null && ancestor is! RenderObjectElement)
      ancestor = ancestor._parent;
    return ancestor;
  }
Copy the code

It is simply an infinite loop. As you can see, there are two conditions for the loop to exit: (1) its parent node is empty and (2) its parent node is RenderObjectElement. This method will find the RenderObjectElement object closest to the current node, and then insertChildRenderObject(renderObject, NewSlot) (this method is executed on the first RenderObjectElement parent of the current node !!!!) While this is an abstract method, let’s look at the override logic in both classes

A, SingleChildRenderObjectElement. InsertChildRenderObject (renderObject newSlot)

  @override
  void insertChildRenderObject(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this
        .renderObject;
    renderObject.child = child;
  }
Copy the code

If finding nodes is SingleChildRenderObjectElement, process is very simple, remove father renderObject RenderObjectElement object, and let the child attribute is equal to the incoming renderObject before us. Note that this is the RenderObject child property, not the Element child property! , so I hang my RenderObject on the RenderObject tree. For example, the Column in the demo is the following procedure1, the first Column of the element object up to find, find the parent node DecoratedBox corresponds to the element object is a subclass of RenderObjectElement SingleRenderObjectElement node (list).

2, called after SingleRenderObjectElement (DecoratedBox node) insertChildRenderObject (RenderObject child, Dynamic Slot). The first argument passed in here is Cloumn’s renderObject, RenderFlex. Insert your own renderObject SingleRenderObjectElement (DecoratedBox node) on the attributes of the child.

Second, MultiChildRenderObjectWidget. InsertChildRenderObject (renderObject newSlot)

If find the nearest node is MultiChildRenderObjectWidget will call its corresponding renderObject. Insert (child, after: slot? The renderObject) method

 @override
  void insertChildRenderObject(RenderObject child, Element slot) {
    final ContainerRenderObjectMixin<RenderObject,
        ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject; renderObject.insert(child, after: slot? .renderObject); }Copy the code

Finally the method call ContainerRenderObjectMixin. InsertIntoChildList ()

What is this ContainerRenderObjectMixin? Check out his comments

/// Generic mixin for render objects with a list of children.
///
/// Provides a child model for a render object subclass that has a doubly-linked
/// list of children.
mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> on RenderObject 
Copy the code
  • Generic mixin, used to render an object with a set of child objects.
  • Provides a submodel for a subclass of the rendered object that has a list of bidirectional linked subclasses.

Find the point! Two-way linked list!! So we probably know MultiChildRenderObjectWidget child nodes through two-way linked list links, the following logic must be to build the list

void _insertIntoChildList(ChildType child, { ChildType after }) {
    final ParentDataType childParentData = child.parentData;
    _childCount += 1;
    if (after == null) {
      // insert at the start (_firstChild)
      childParentData.nextSibling = _firstChild;
      if(_firstChild ! =null) {
        finalParentDataType _firstChildParentData = _firstChild.parentData; _firstChildParentData.previousSibling = child; } _firstChild = child; _lastChild ?? = child; }else {
      final ParentDataType afterParentData = after.parentData;
      if (afterParentData.nextSibling == null) {
        // insert at the end (_lastChild); we'll end up with two or more children
        childParentData.previousSibling = after;
        afterParentData.nextSibling = child;
        _lastChild = child;
      } else {
        // insert in the middle; we'll end up with three or more children
        // set up links from child to siblings
      	/ /... Ignore the following logic......... /}}}Copy the code

This method takes two arguments: the first child represents the renderObject we are currently passing in, and the second represents the node that preceded it (so it might be empty). From the comments above, we know that this method does three things: (1) place the child on the first node when after is null; (2) insert the child node into the end of the list; (3) insert the child node into the middle of the list. It’s a little bit clearer if you combine it with an example.

The Column in this example has four children:

The first child, SizeBox, calls this method after it finds Column(RenderObjectElement) up, and then after is empty, So firstChild is RendenrConstrainedBox(RenderObject for SizeBox)

The second child node in the demo is Text. As mentioned earlier, Text is the widget of the composite class so it does not participate in the Render tree mount, but RichText. Column(RenderObjectElement); RenderObjectElement (RenderObjectElement); 1, child-> RichText (RenderObject) 2, After -> SizeBox (RenderObject) Execute the following code:

  // insert at the end (_lastChild); we'll end up with two or more children
 final ParentDataType afterParentData = after.parentData;
      if (afterParentData.nextSibling == null) {
        // insert at the end (_lastChild); we'll end up with two or more children
        childParentData.previousSibling = after;
        afterParentData.nextSibling = child;
        _lastChild = child;
      } 
Copy the code

Point the previousSibling of the current child node ParentData to the first node, point the nextSibling attribute of the first node ParentData to the current child, and finally point _lastChild to itself.

So at the end of the process, we’re going to get a RenderTree that looks like this


conclusion

This is a seemingly simple demo for usWith an inward-only page structure, the three trees look like this:

If that’s hard to understand, I’ve been using setState() incorrectly, right? And the interviewer asked me about the life cycle of State. How should I answer that I am familiar with the construction process of Flutter

Recommended Reading:

Advanced Guide to Flutter

Have I been using setState() incorrectly?

The interviewer asked me about the life cycle of State. What should I say

After 30 examples, I understood the layout of the Flutter