In order to avoid the drudness of the traditional source code presentation, this time I will try to take a different approach and bring you a relaxed understanding of the UI drawing process in the Flutter world, exploring the secrets of Widgets, Elements and RenderObjects.

Without further ado, listen to the story! 2. The Jungle Book

The story

Ten years of war, mobile terminal pattern gradually set, clear barriers.

Although the northern grassland golden Ledger dynasty internal disputes, but has been peeping into the central plains, years of harassment, now has won a small piece of territory (ReactNative. Folk popular: big front-end integration of the potential is now!

In the winter of 2018, Flutter, a small city on the border of Android, suddenly declared its statehood. And declare war on both mobile empires!! In just a few days, several cities have been taken.

And today we are going to tell the story, took place in the most heavily fought Android border town: View City.

One day, the Android View City military conference:

The general of the town said to his advisor, “Flutter has attacked us several times recently, and has already conquered several cities. Who can tell me the difference between this Flutter and our current View?”

The lower counsellor looked at each other, but finally a counsellor stood out: “I would like to go to inquire for the general!”

A few days later, advisor: “I came back and found that the main difference between Flutter and our View City lies in the programming paradigm and View LOGICAL unit.”

General: “First, how are programming paradigms different?”

Android/Flutter programming paradigm

General, our Android View development is now imperative, each of our views are directly under the command of general (Developer), for example: want to change a text interface, to specify the specific TextView call his setText method command text change;

The view development of Flutter is declarative. All the general needs to do is to maintain a data set, set up a WidgetTree, and “bind” the Widget to some data in the data set to render against that data. For example, when a copy needs to be changed, the data in the dataset is changed, which directly triggers the WidgetTree re-rendering. This way, Flutter generals no longer need to focus on individual soldiers, but instead focus most of their efforts on maintaining core data.

If every operation costs a little checkmate energy, the same data happens to be “bound” to multiple views or widgets. Imperative programming requires ordering N views to change, costing N points of effort;

All declarative programming needs to do is change the data + trigger WidgetTree redraw, which costs 2 energy; This liberation of energy is one of the reasons why Flutter can attract so many generals so quickly.

General: “But every time the data changes, it will trigger the WidgetTree redraw. The resource consumption is too much, I now use more energy, but there will not be a lot of object creation.”

Widget, Element, RenderObject concepts

Tactician: And that’s the second difference I’m going to talk about. Because WidgetTree does a lot of redrawing, widgets are necessarily cheap.

The Flutter UI has three elements: Widget, Element and RenderObject. There are three owners who manage them: WidgetOwner(General &Developer), BuildOwner and PipelineOwner.

  • Widgets, widgets are not real soldiers, they are just pieces in the hands of generals, cheap, pure objects that hold configuration information for rendering and are constantly being replaced.

  • RenderObject is the real soldier who fights with us. Conceptually, like our Android View, RenderObject is used by the rendering engine for real rendering, which is relatively stable and expensive.

  • Element is responsible for converting ever-changing widgets into relatively stable RenderObjects.

WidgetOwner (Developer) keeps changing the deployment plan, then sends BuildOwner WidgetTree after WidgetTree, and the first WidgetTree generates a corresponding ElementTree, And generate the corresponding RenderObjectTree.

BuildOwner then compares each time it receives a new schedule to the last one, updates only the changed portion of the ElementTree, Element may be updated only, or it may be replaced. After Element is replaced, The corresponding RenderObject is replaced.

You can see that the WidgetTree has all been replaced, but ElementTree and RenderObjectTree have only replaced the changed parts.

PipelineOwner, which is similar to ViewRootImpl in Android, manages the View that needs to be drawn. Finally PipelineOwner flush the changed nodes in RenderObjectTree and render them to the underlying engine.

General: “I get it. It seems that the core of declarative programming stability lies in this Element and BuildOwner. But I see there are still two problems here, the RenderObject is missing a node, right? Did you draw the picture wrong? Can you also tell me how he links widgets to RenderObject and how BuildOwner Diff elements when changes are made?”

Relationships between Widgets, Elements, and RenderObjects

First, the older Widget of each Widget family gives all Widget subclasses three Key capabilities: a Key that ensures its own uniqueness and location, a createElement that creates an Element, and a canUpdate. The purpose of canUpdate will follow.

Another particularly good and strong Widget subclass is RenderObjectWidget, which on paper represents the rendering capability, and it also has a createRenderObject method for creating RenderObjects.

As you can see, the Widget, Element, and RenderObject creation relationships are not linear. The Element and RenderObject are created by the Widget. Not every Widget has a Corresponding RenderObjectWidget. This also explains that RenderObjectTree in the figure above looks to be missing some nodes from the previous WidgetTree.

The first creation and association of widgets, Elements, and RenderObjects

When you talk about the first creation, you have to start with the first soldier that was created. We all know about Android ViewTree:

-PhoneWindow
	- DecorView
		- TitleView
		- ContentView
Copy the code

There are already so many views up front. Compared to the Android ViewTree, the WidgetTree of Flutter is much simpler, with only the root widget at the bottom level.

- RenderObjectToWidgetAdapter < RenderBox > - MyApp (custom) - MyMaterialApp (custom)Copy the code

A brief introduction of RenderObjectToWidgetAdapter, don’t be trapped by his adapter name is confused and RenderObjectToWidgetAdapter is actually a RenderObjectWidget, It was the first good, strong Widget.

This time you have to move out of the code to see,runApp source code:

void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code

The WidgetsFlutterBinding superstition is a set of bindings that hold some of the owners described above, such as BuildOwner, PipelineOwner, So as the WidgetsFlutterBinding initializes, other bindings are also initialized, and the state engine of the Flutter starts to spin!

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
  }
Copy the code

The most important thing to look at is the attachRootWidget(app) method, which is very sacred and has been implemented for the first time. (General: “Holy? You won’t mutiny?” ), the app is our incoming custom Widget, internal creates RenderObjectToWidgetAdapter, and the app as its child.

This is followed by the attachToRenderTree method, which is also sacred, which creates the first Element and RenderObject

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
    if(element == null) { owner.lockState(() { element = createElement(); // Create rootElement element.assigNowner (owner); // bind BuildOwner}); Owner. buildScope(Element, () {// Initialization of child widgets starts here element.mount(null, null); // Execute rootElement mount before initializing the child Widget}); }else{... }return element;
  }
Copy the code

To explain the above image, Root is relatively easy to create:

  • 1.attachRootWidget(app)Method to create the Root/Widget (namely RenderObjectToWidgetAdapter)
  • 2. Call immediatelyattachToRenderTreeMethod to create Root[Element]
  • 3.Root[Element] attempts to callmountMethod to mount itself to the parent Element, because it is root, so there is no parent Element, mount empty
  • 4. The Widget will be called during mountcreateRenderObjectCreate Root[RenderObject]

How does its child, the app that we passed in, mount the parent control?

  • 5. We will app passed as a parameter to Root Widget (namely RenderObjectToWidgetAdapter), app/Widget becomes as Root child [Widget] [Widget]
  • 6. Callowner.buildScopeTo create and mount the subtree, type the blackboard!! This process is exactly the same as the WidgetTree refresh process, which will be covered later!
  • 7. CallcreateElementMethod to create Child[Element]
  • 8. Call Element’smountMethod, mount itself to Root to form a tree
  • 9. Mount while callingwidget.createRenderObjectTo create the Child [RenderObject]
  • 10. After the creation is complete, invokeattachRenderObjectTo complete the link with Root[RenderObject]

In this way, WidgetTree, ElementTree, and RenderObject are created and linked to each other.

General: “I want to take a look at the mount and attachRenderObject process to see how it’s mounted.”

Abstract class Element: void mount(Element parent, dynamic newSlot) {_parent = parent; // Hold a reference to the parent Element _slot = newSlot; _depth = _parent ! = null ? _parent.depth + 1 : 1; // Depth of the current node _active =true;
    if(parent ! = null) // Only assign ownershipifthe parent is non-null _owner = parent.owner; // Each Element's buildOwner comes from its parent class, buildOwner... }Copy the code

Let’s take a look at the mount of an Element and let _parent hold a reference to the parent Element

Because RootElement has no parent Element, null is passed: element.mount(null, null);

There are two other things to note:

  • The depth of the node is also calculated at this time, depth is important for the refresh! Write down the first!
  • Each Element’s buildOwner comes from the parent class’s buildOwner, which ensures that an ElementTree is maintained by only one buildOwner.
abstract class RenderObjectElement: @override void attachRenderObject(dynamic newSlot) { ... _ancestorRenderObjectElement = _findAncestorRenderObjectElement(); _ancestorRenderObjectElement? .insertChildRenderObject(renderObject, newSlot); . }Copy the code

Mounting RenderObject and its parent RenderObject is a little more complicated. We can see from the code that we need to check our AncestorRenderObject first, why?

Remember that every Widget has a corresponding Element, but that Element doesn’t necessarily have a corresponding RenderObject. So when your parent Element doesn’t have a RenderObject, you need to look up.

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

RenderObjectElement (RenderObjectElement, RenderObjectElement, RenderObjectElement, RenderObjectElement) At this point, the RenderObject is being mounted between its children.

The refresh process of Flutter: Reuse of elements

From the previous section, we know that although the implementation of the createRenderObject method is in the Widget, it is Element that holds the RenderObject reference. Forget? Let’s look at the code:

abstract class RenderObjectElement extends Element {
	...
	
  @override
  RenderObjectWidget get widget => super.widget;

  @override
  RenderObject get renderObject => _renderObject;
  RenderObject _renderObject;
}
Copy the code

Element holds both, so to speak, Element is the middleman between the Widget and the RenderObject, and it does make a profit…

At this point, the Root Widget, Root Element, and Root RenderObject are all created and linked successfully. Do you have any questions, General?

General: “There are brokers inside Flutter to make the difference? Corruption! Tell me how he made the difference? Maybe I can learn.”

If Flutter wants to refresh the page, it calls the setState() method in the StatefulWidget.

@protected
void setState(VoidCallback fn) {
	...
	_element.markNeedsBuild();
}
Copy the code

General let’s practice. Suppose that Flutter sends a WidgetTree like this:

Refresh Step 1: Element marks itself as dirty and tells buildOwner to handle it

SetState ((){_title=” TTT “}) is called inside the StatefulWidget when the other party wants to change the Text in the Text Widget below. The widget’s corresponding element then marks itself as dirty and calls owner.scheduleBuildFor(this); Notify buildOwner for processing.

The subsequent StatefulWidget build method will be executed, and a new child Widget will be created. The original child Widget will be discarded (General: “A good object is wasted… Young people nowadays ~ “).

The original child widgets are definitely dead, but their Element will most likely be.

Refresh Step 2: buildOwner adds element to the collection _dirtyElements and tells UI.window to schedule a new frame

BuildOwner adds all dirty elements to _dirtyElements and waits for the next frame to be drawn.

Ui.window.scheduleframe () is also called; Notifies the underlying rendering engine to schedule a new frame processing.

Refresh Step 3: The underlying engine finally calls back to the Dart layer and executes buildOwner’s buildScope method

This is important, so use code to make it clear!

void buildScope(Element context, [VoidCallback callback]){
	...
}
Copy the code

buildScope!! Remember that? We saw earlier in Root creation that the first creation of the Child was also called with the buildScope method! Tree first frame creation and refresh is a set of logic!

BuildScope needs to pass in an Element argument, which we should understand from the literal, presumably rebuilding the scope below the Element.

void buildScope(Element context, [VoidCallback callback]) { ... try { ... / / 1. Sorting _dirtyElements. Sort (Element) _sort); . int dirtyCount = _dirtyElements.length; int index = 0;while(index < dirtyCount) { try { //2. Traverse rebuild _dirtyElements [index]. Rebuild (); } catch (e, stack) { } index += 1; } } finally {for (Element element in _dirtyElements) {
        element._inDirtyList = false; } //3. Clear _dirtyElements. Clear (); . }}Copy the code
Step 3.1: Sort _dirtyElements from smallest to largest by Element depth

Why sort? The build method of the parent Widget must trigger the build of the child Widget. If the child Widget is built first and then the parent Widget is built again, the child Widget will be built again. So by sorting in this way, you avoid duplicate builds of child widgets.

Step 3.2: Iterate through the rebuild method of element in _dirtyElements

It is important to note that new elements may be added to the _dirtyElements set during the traversal. In this case, the length of the _dirtyElements set is used to determine whether new elements are added, and if so, the order is reordered.

Element’s rebuild method eventually calls performRebuild(), and performRebuild() has different implementations for different elements

Step 3.3: After traversal, clear the collection of dirtyElements

Refresh Step 4: Execute performRebuild()

Different elements have different implementations of performRebuild(), so let’s just look at the two most common elements for now:

  • ComponentElement is the parent of StatefulWidget and StatelessElement
  • RenderObjectElement is the parent class of an Element with rendering capabilities
ComponentElement performRebuild ()
void performRebuild() { Widget built; try { built = build(); }... try { _child = updateChild(_child, built, slot); }... }Copy the code

Execute Element’s build(); , take the StatefulElement build method as an example: Widget build() => state.build(this); . This is the build method that executes the state of the StatefulWidget we copied

What happens when you execute the build method? A child Widget of the StatefulWidget, of course. Here we go! Knock on the blackboard!! General: “Knock on the blackboard again?” (This is where Element makes its difference!

Element updateChild(Element child, Widget newWidget, dynamic newSlot) { ... / / 1if (newWidget == null) {
      if(child ! = null) deactivateChild(child);return null;
    }
    
    if(child ! = null) { //2if (child.widget == newWidget) {
        if(child.slot ! = newSlot) updateSlotForChild(child, newSlot);returnchild; } / / 3if (Widget.canUpdate(child.widget, newWidget)) {
        if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);returnchild; } deactivateChild(child); } / / 4return inflateWidget(newWidget, newSlot);
  }
Copy the code

The child argument is the child Element that Element was mounted last time, and the newWidget was just built. There are four possible scenarios for updateChild:

  • 1. If the newly built widget is null, the widget is removed, and the Child Element can be removed.

  • 2. If the child widget is the same as the newly built widget, return the widget if it is not. Element is the old Element

  • 3. Check whether the Widget can be updated. The logic of widget. canUpdate is to determine whether the key value is equal to the runtime type. If the conditions are met, it updates and returns.

Where does the middleman’s price difference come from? As long as a new Widget is built with the same type and Key as the last one, Element will be reused! This ensures that while widgets are being created all the time, the Element is relatively stable as long as there are no major changes, and thus the RenderObject is stable!

  • 4. If the preceding three conditions are not met, the call is invokedinflateWidget()Creating a new Element

Take a look at the inflateWidget() method again:

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if(newChild ! = null) { newChild._activateWithParent(this, newSlot); final Element updatedChild = updateChild(newChild, newWidget, newSlot);return updatedChild;
      }
    }
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    return newChild;
  }
Copy the code

If this fails, call the Widget’s method to create a new Element. Then call the mount method to mount itself to the parent Element. A new RenderObject will be created in this method.

RenderObjectElement performRebuild ()
@override
  void performRebuild() {
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
Copy the code

Unlike ComponentElement, the RenderObject is updated by calling the updateRenderObject method instead of building.

Different widgets also have different updateRenderObject implementations, so let’s take a look at the most commonly used RichText, also known as Text.

void updateRenderObject(BuildContext context, RenderParagraph renderObject) { assert(textDirection ! = null || debugCheckHasDirectionality(context)); renderObject .. text = text .. textAlign = textAlign .. textDirection = textDirection ?? Directionality.of(context) .. softWrap = softWrap .. overflow = overflow .. textScaleFactor = textScaleFactor .. maxLines = maxLines .. locale = locale ?? Localizations.localeOf(context, nullOk:true);
  }
Copy the code

Some of the assignment operations that look familiar look like Android views. The RenderObject is actually a View on Android.

Now you can see how Element handles the variability of widgets in the middle, keeping RenderObject relatively constant

Flutter refresh process: PipelineOwner manages RenderObject

At the bottom the engine eventually goes back to the Dart layer and finally executes the WidgetsBinding drawFrame ()

WidgetsBinding

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

buildOwner.buildScope(renderViewElement); That’s what we talked about above.

Now look at super.drawframe (); PipelineOwner manages RenderObject at PipelineOwner, which we’ll cover in more detail in the next installment.

@protected
  void drawFrame() { assert(renderView ! = null); pipelineOwner.flushLayout(); . / / layout need to be layout RenderObject pipelineOwner flushCompositingBits (); // Determine whether the layer changes to pipelineowner.flushpaint (); / / drawing need to be painted RenderObject renderView.com positeFrame (); / / this sends the bits to the GPU to draw good layer to the engine map pipelineOwner. FlushSemantics (); // this also sends the semantics to the OS. Some semantic scenarios require}Copy the code

The refresh process of Flutter: Cleanup

The drawFrame method finally executes buildowner.finalizetree ();

void finalizeTree() {
    Timeline.startSync('Finalize tree', arguments: timelineWhitelistArguments); try { lockState(() { _inactiveElements._unmountAll(); // this unregisters the GlobalKeys }); . } catch (e, stack) { _debugReportException('while finalizing the widget tree', e, stack); } finally { Timeline.finishSync(); }}Copy the code

Just doing the final cleanup.

General: what is _inactiveElements? Why haven’t you seen that before?

Remember the updateChild method Element used to make the price difference? All unused elements are recycled by calling the deactivateChild method:

  void deactivateChild(Element child) {
    child._parent = null;
    child.detachRenderObject();
    owner._inactiveElements.add(child); // this eventually calls child.deactivate()
  }
Copy the code

This is where the deprecated element is added to _inactiveElements.

In addition, after the element is discarded, the inflateWidget is called to create a new element, and the _retakeInactiveElement is called to attempt to reuse the element via GlobalKey. The reuse pool is also in _inactiveElements.

If you don’t reuse Element with a GlobeKey ina frame, _inactiveElements will be cleared at the end and cannot be reused.

At the end

General, do you now have a preliminary understanding of the Flutter drawing process?

General: “Somewhat understood, but with all you’ve said, doesn’t the Flutter drawing process sound too bad compared to our Android drawing process?”

Of course there is. We only know the tip of the iceberg about Flutter.

But just by dynamically inserting components into a ViewTree, Flutter is not as flexible as we are. Because Flutter is declarative, there is no mature interface for inserting widgets into WidgetTree at any time while Flutter is running.

However, it is believed that this problem will be solved soon as Flutter developers become more familiar with the inner workings of Flutter.