This is the seventh day of my participation in the August More text Challenge. For details, see: August More Text Challenge

preface

In the last post, we compared the setState and ModelBinding state management methods. As a result, the performance of setState is significantly lower than that of ModelBinding using InheritedWidget. This is because setState, regardless of whether or not the component is dependent on state data, will be rebuilt after all the subcomponents have been removed. So what does the setState process do to cause this? In this article we analyze the setState process through the source code of Flutter.

The definition of setState

Let’s first look at the setState definition. SetState is defined in the State

with Diagnosticable class, which is the State class of the StatefulWidget or its subclass. Method body code is not much, in the execution of business code to do some exception handling, we do not paste the specific code, mainly to do the following processing:

  • To pass tosetStateThe callback method for cannot be empty.
  • Lifecycle validation: Components that have been removed from the component tree aredisposeDrop, therefore can not indisposeAfter the callsetState. This usually happens during timers, animations, or asynchronous callbacks. Such calls can result in memory leaks.
  • increatedPhase and no loading phase (mounted) cannot be calledsetStateThat is, cannot be called in a constructorsetState. Usually it should beinitStateAfter the callsetState.
  • The setState callback method cannot return a Future objectsetStateIs used to perform asynchronous operations. What if you want to perform an asynchronous operationsetStateOutside of the call.
@protected
void setState(VoidCallback fn) {
  // Omit the exception handling code_element! .markNeedsBuild(); }Copy the code

The most important is a single line of code: _element! .markNeedsbuild (), which from the function name means that the tag element needs to be built. So where does this _element come from? Continue to dig!

What is Element?

If we look at the definition of _element, _element is a StatefulElement object, and in fact, we see that when we get BuildContext, we also return _element. Here’s what the comment says when we get BuildContext:

The location in The tree where this Widget builds will be built.

BuildContext is an abstract class, so you can infer that StatefulElement is actually its interface implementation class or subclass. Looking back, the class hierarchy looks like this, where Element and ComponentElement are abstract classes, and the markNeedsBuild method is defined in the Element abstract class. The official definition of Element is:

An instantiation of a Widget at a particular location in the tree.

An Element is the object that Bridges the Widget configuration to the rendering tree, meaning that the actual rendering process is more controlled by the Element.

classDiagram
    BuildContext <|.. Element
    DiagnosticableTree <|-- Element
    Element <|-- ComponentElement
    ComponentElement <|-- StatefulElement
    class Element {
        Element(Widget widget)
        +_sort(Element a, Element b)

        -reassemble()
        -markNeedsBuild()
        -get renderObject
        -updateChild(Element? child, Widget? newWidget, dynamic newSlot)
        -mount(Element? parent, dynamic newSlot)
        -unmount()
        -update(covariant Widget newWidget)
        -detachRenderObject()
        -attachRenderObject(dynamic newSlot)
        -deactivateChild(Element child)
        -activate()
        -didChangeDependencies()
        -markNeedsBuild()
        -rebuild()
        -performRebuild()

        -Element? _parent
        -int _depth
        -Widget _widget
        -BuildOwner? _owner
        _ElementLifecycle _lifecycleState
    }

The above diagram lists our Element’s key attributes and methods.

  • The _depth attribute: the level of the element in the component tree. This value must be greater than 0 for the root node.

  • The _sort method compares the level of two elements a and B. The larger the level value (_depth), the deeper the level, and the higher the level displayed.

  • _parent: Parent node element, which may be empty.

  • _widget: Configures the component configuration of the element (actually the Widget object, the Widget itself is the configuration parameter of the rendered element, not the actual rendered element).

  • _owner: The object that manages the element declaration period.

  • _lifecycleState: Life cycle state property, initial by default.

  • The get method for getting renderObject: recursively calls the object that returns the element and its children (the child is the RenderObjectElement object) to render.

  • Reassemble: The reassemble method, which is used only during the debug phase, such as hot overload. This method handles marking the element itself as outside of build (calling the markNeedsBuild method), and recursively traversing all the child nodes, calling their reassemble method.

  • UpdateChild: This is the core method of the rendering process to update the specified child elements with a new component configuration. There are four combinations: – If child is empty and newWidget is not, then a new element is created to render:

    • ifchildIt’s not empty, butnewWidgetEmpty, it indicates that the component is not configuredchildThis element, so I need to remove it.
    • If neither of them is null, then the basis forchildCan the current update (Widget.canUpdate), and update the element with the new component configuration, if possible; Otherwise we need to remove the old element and create a new one with the new component configuration.
    • If both are empty, then do nothing.

The results are returned in three cases:

1. If a new element is created, the newly constructed child element is returned. 2. If the old element was updated, return the updated child element. 3. Return null if the child element was removed without a new replacement.Copy the code
  • mountMethod: This method is called when the new element is first created to insert the element into the given parent node at the given slot. When this method is called, the state of the element is changed frominitialInstead ofactive. It also sets the child element’s level (_depth) to the parent element’s level +1.
  • updateMethod: When the parent node uses the new configuration component (newWidgetThis method is called when an element is changed. The new configuration type is required to be consistent with the old one.
  • detachRenderObjectandattachRenderObjectRemove renderObject from component tree and add renderObject to component tree, respectively.
  • deactivateChildMethod: Add the child element to the inactive element list before removing it from the render tree.
  • activateMethod: Called when the state is switched from Inactive to active. It is a lifecycle function. Note that this method is not called the first time the component is mounted, but the mount method.
  • deactivateMethod: called when the state is switched from active to inactive, that is, when an element is moved to the inactive list.
  • unmountMethod: Called when the state switches from Inactive to defunct(no longer exists), at which point the element leaves the render tree and never exists in the render tree again.
  • didChangeDependenciesThis method is also called when an element’s dependencies changemarkNeedBuildMethods.
  • markNeedsBuildMethod: Marks the element asdirtyState in order to reconstruct the element when the next frame is rendered. The core of this approach is to do the following:
_dirty = true; owner! .scheduleBuildFor(this)
Copy the code
  • rebuildMethod: when the element’sBuildOwnerThe objectscheduleBuildForMethod is calledrebuildMethod to reconstruct the element. It was first loaded atmountMethod when a configuration component changesbuildMethod trigger. This method is calledperformRebuildMethod to reconstruct the element.performRebuildIs a method implemented by a word class with elements, that is, how each Element is reconstructed is determined by the subclass.

Looking at a lot of content, let’s take a look at the rendered state flow, which is a state diagram for the life cycle of an element. The component is removed and appears in the deactivate method, which triggers the deactivate method when an element is moved to the inactive element list. The way to move elements to the inactive list is deactivateChild, an operation on the parent node — when a child element is no longer part of the rendered tree built by the parent, it is added to the inactive list.

Mount --> Mount --> Mount --> Mount --> Mount --> Mount --> Mount --> Mount --> Mount --> Active (active (active)) ((active)) - deactivate - > inactive ((inactive)) is not active ((inactive)) - unmount to -- > no longer exists ((defunct)) no longer exists ((defunct)) -- - >  dispose

performRebuildmethods

Now that we know that when we setState, we actually call performRebuild to rebuild the component tree, what does performRebuild do? In Element, the performRebuild method is an empty method that needs to be subclassed to implement. So let’s go to StatefulElement and look for the following code:

@override
void performRebuild() {
  if (_didChangeDependencies) {
    state.didChangeDependencies();
    _didChangeDependencies = false;
  }
  super.performRebuild();
}
Copy the code

Still have to look up, that is ComponentElement, finally found!

@override
void performRebuild() {
  // Omit the debug code
  Widget? built;
  try {
    // ...
    built = build();
    // ...
  } catch (e, stack) {
    // ...
  } finally {
    // We delay marking the element as clean until after calling build() so
    // that attempts to markNeedsBuild() during build() will be ignored.
    _dirty = false;
    // ...
  }
  try {
    _child = updateChild(_child, built, slot);
    assert(_child ! =null);
  } catch (e, stack) {
    // Omit exception handling
  }
  // Omit debugging code
}
Copy the code

The key here is that the build method and the updateChild method are called. The latest Widget is retrieved from built = build(), and because the build method rebuilds the component configuration, the constructor and build method of the corresponding Widget are called. The child element is then updated by calling the updateChild method. As mentioned earlier, there are three combinations of updateChild updateChild components. In our case, _child and built are definitely not empty. The key is whether the built canUpdate is true. This method is defined in the Widget class:

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType &&
      oldWidget.key == newWidget.key;
}
Copy the code

The note states that if the Widget’s key is not set (it is not recommended for components to set keys), then the runtimeType of the two components can be updated with the same runtimeType. So, in fact, most of the time it returns true. When we debug the update code, we end up in this branch of Element’s updateChild:

// ...
else if (hasSameSuperclass &&
          Widget.canUpdate(child.widget, newWidget)) {
  if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);assert(child.widget == newWidget);
  assert(() { child.owner! ._debugElementWasRebuilt(child);return true; } ()); newChild = child; }Copy the code

From this we can infer that the setState method does rebuild the entire Widget, but it does not necessarily remove every Element in the Widget’s configured Element tree and rerender it with a new Element. In fact, when we turned on the Debugging tool of the Flutter, we saw that the actual Element of the Widget did not change after clicking the button.

conclusion

The setState call does not rebuild all the elements in the Element layer of the render control layer, as it does in the Widget layer. However, this does not mean that setState is ok. First, as mentioned in the previous chapter, it will rebuild the entire Widget tree, which will incur a performance cost. Second, since the entire Widget tree has changed, this means that the entire tree’s render layer Element objects will perform the update method, which may or may not be re-rendered, but the performance overhead of traversing the entire tree is high. Therefore, for performance reasons, try not to use setState — unless the component is really simple and there are few or no sub-components.


I’m an island user with the same name on wechat. This is a column on introduction and practice of Flutter.

👍🏻 : feel a harvest please point to encourage!

🌟 : Collect articles, convenient to read back!

💬 : Comment exchange, mutual progress!