Flutter Quick Learning and Quick Use 24 Lectures–23 Architecture Principles: Why Flutter Has Better Performance

In this class, we will continue to delve into the source code and learn the principles of Flutter rendering, especially why Flutter can maintain a relatively good performance experience.

Performance advantages

Before this, the industry has always said that Flutter’s performance is better than other cross-end technical frameworks, and it is basically the same as the native platform experience. So how is it done? Before understanding Flutter’s self-rendering principle, let’s take a look at how the native platforms Android and iOS render UI. After the comparison before and after, it can better reflect the characteristics that its performance is almost the same as that of the original.

Basic principles of UI rendering

Let’s first explain a basic knowledge point, how to implement the UI interface and operating system that we see every day, refer to the rendering process in Figure 1 below.

Drawing 0.png

Figure 1 The drawing principle of the system UI interface

From Figure 1, we can see that when an interface is displayed, the data is calculated by the CPU first, and then the data is sent to the GPU. The GPU then draws a pixel interface according to the corresponding data, and then puts it into the frame buffer area. Finally, The display regularly obtains frame data from the frame buffer and displays them on the display.

In the above rendering implementation process, communication between CPU and GPU is required. Therefore, how to schedule the GPU is a key point. There is currently a set of specifications called OpenGL, through which developers can call the GPU for interface rendering more conveniently and efficiently. Both Android and iOS systems implement this set of functions at the system level, and package them into SDK APIs. This set of rules is also implemented in Flutter, that is, a set of Dart API is encapsulated by applying the OpenGL specification, so the rendering principle of Flutter is consistent with that of Android and iOS, so there is basically no difference in performance.

After understanding the rendering principle of Flutter, let’s take a look at the rendering principles of the two commonly used cross-end frameworks.

Rendering principles of other cross-end technical frameworks

The two most common cross-end technical frameworks are ReactNative and Weex. They are very similar in principle, so the principle of ReactNative is introduced here separately. Let’s first look at a technical architecture in Figure 2 below.

Drawing 2.png

Figure 2 ReactNative technical architecture diagram

From Figure 2, we can clearly see that ReactNative is completely based on the native platform for rendering, and the communication between them is mainly through JSbridge, and the data that needs to be rendered is passed to the native platform through JSbridge. This kind of communication method also exists in Flutter. It was introduced in our Lesson 20 “Native Communication: Applying Native Platform Interaction to Expand Flutter’s Basic Capabilities”. This part is similar to ReactNative. The biggest difference between the two is that the Flutter UI interface is self-rendering, while ReactNative informs the native platform through communication, and then the native platform draws the interface. .

Let’s go back to Hybrid, the most original cross-end technology framework. It uses H5 on the interface, and other functions use JSbridge to call native services. Therefore, it does not use the native drawing interface, but only uses the native platform capabilities.

The above is a comparison of the three technical frameworks. Let’s summarize the problems that the three frameworks solve prominently, and then explain the problems that exist in the current framework.

  • Hybrid only supports native capabilities in Figure 2, such as camera, storage, calendar, etc., while the UI interface is still H5, so both experience and performance are relatively poor.

  • In order to solve the page performance problem, ReactNative also uses the JSbridge communication method to pass the virtual DOM and page rendering related data to the native platform, and the native platform draws the native experience interface based on the virtual DOM and rendering related data, so that the user perceives It is a native interface, but JavaScript code parsing and running is required in this process, and then communicates with the native platform, resulting in a certain performance loss.

  • In order to solve the above problems, Flutter further optimizes this experience. Flutter does not rely on native rendering capabilities, but implements a set of rendering principles that are the same as Android and iOS, so that its performance is basically the same as that of native platforms. However, since Flutter is currently only a UI framework, it still needs to rely on the native platform in terms of native functions, which is also some of its problems.

Rendering principle

After understanding the particularity of Flutter’s rendering principle, let’s take a closer look at how the entire rendering process is implemented. In the last lesson, I introduced the conversion process of the three trees, then it is necessary to further analyze how to render the three trees as a UI interface. Before introducing how the converted three trees are drawn into a UI interface, let’s first understand the concept of vsync.

vsync

From Figure 1, we can see that the video controller will obtain the frame data to be displayed from the frame buffer and display it on the display. The display has a refresh rate (such as 60 Hz or 120 Hz), which means that the display will obtain 60 frames of data per second, that is, every 1000 ms / 60 = 16.67 ms to regularly obtain frame data from the video controller. This is what we often call a concept “vertical synchronization signal” (vsync).

Flutter’s self-rendering mode also follows this principle. Therefore, in terms of Flutter’s performance requirements, the UI thread processing time plus the GPU drawing time must be less than 16.67 ms to prevent frame drops. After mastering the concept of vysnc, let’s take a look at how the internal logic of Flutter is implemented.

Rendering process

The core function process involved in the entire drawing process is shown in Figure 3.

Drawing 3.png

Figure 3 Draw the overall flow chart

In the process of Figure 3, several important functions are involved: scheduleWarmUpFrame, handleDrawFrame, drawFrame, flushLayout, flushCompositingBits, markNeedsPaint, flushPaint, compositeFrame and flushSemantics. Next, let’s take a look at the role of these functions.

Important functions

  • scheduleWarmUpFrame, the core of this function is to call handleBeginFrame and handleDrawFrame two methods.

  • handleDrawFrame mainly executes the callback function list of _persistentCallbacks. Many execution functions are stored in _persistentCallbacks, among which the most important function RenderBing’s drawFrame is stored. This method is mainly stored in _persistentCallbacks through the WidgetsFlutterBinding binding phase.

  • drawFrame, in the function, mainly performs the drawing work of the interface, and executes the flushLayout, flushCompositingBits, flushPaint, compositeFrame and flushSemantics functions in turn.

  • flushLayout, which updates the layout information of all RenderObjects marked as “dirty”. The main action takes place in the node._layoutWithoutResize() method, which calls performLayout() for re-layout calculations. Please note that performLayout here will call different performLayout layout methods according to different types of RenderObjects. This method also calls markNeedsPaint to mark the RenderObject that needs to be redrawn. The source code is as follows:

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

  • flushCompositingBits, mainly checks whether the RenderObject and child nodes need to create a new layer. If necessary, the _needsCompositing attribute is marked as true, and then the parent node is cyclically judged. If the parent node needs a new layer, the flag bit also needs to be set to true, if the layer changes, it will eventually call markNeedsPaint to repaint. Some source codes are as follows:
visitChildren((RenderObject child) {
  child._updateCompositingBits();
  if (child. needsCompositing)
    _needsCompositing = true;
});
if (isRepaintBoundary || alwaysNeedsCompositing)
  _needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)
  markNeedsPaint();

  • markNeedsPaint, this method is similar to markNeedsBuild in Element. Since the current node needs to be repainted, it will loop on the parent node, find the nearest isRepaintBoundary type, and then draw it. If the parent node is not found all the way up, it can only draw the current one. node.

  • flushPaint, loop to judge the RenderObject node that needs to be updated for redrawing, and call PaintingContext.repaintCompositedChild to perform repainting operation. The paint method will be called in repaintCompositedChild. This method is somewhat similar to the update method of Element. It will call different paint methods according to different types of RenderObjects. For example, custom_paint.dart or sliver_persistent_header.dart have implemented their own paint methods. In the specific paint The method will call the canvas api to complete the drawing, and recursively determine the type of child nodes, call different paint methods to complete the final drawing work, and finally generate a Layer Tree, and save the drawing instructions in the Layer.

for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
  assert(node._layer != null);
  if (node._needsPaint & amp; & amp; node. owner == this) {
    if (node._layer.attached) {
      PaintingContext.repaintCompositedChild(node);
    } else {
      node._skippedPaintingOnLayer();
    }
  }
}

  • CompositeFrame converts the scene information drawn by the canvas into binary pixel information and sends it to the GPU to complete the specific interface rendering operation;

  • flushSemantics, which sends the semantics of rendered objects to the operating system, which has little to do with the Flutter drawing process.

The above is the introduction of several very critical core functions. Next, let’s look at the specific execution process.

Process Description

The following are the functions executed in each process, as shown in Figure 4.

image (2).png

Figure 4 Core drawing process execution process

According to the overall process in Figure 3, we know that there are four important functions involved in the drawing process, and Figure 4 illustrates the specific logic executed by these four functions during execution.

  • flushLayout, prepare for layout-related processing work, here will determine whether to re-layout, call performLayout. Since the layout-related implementation of different basic components is different, different performLayout will be called according to different component types to complete the layout-related preparations. At the end of the performLayout processing logic, markNeedsPaint is also called to mark the RenderObject that needs to be redrawn. In performLayout, markNeedsLayout will also be executed to mark which ones need to be re-layouted, which will be used in the specific layout function.

  • flushCompositingBits, prepare the relevant processing logic of the layer, and call markNeedsPaint to mark the RenderObject that needs to be redrawn.

  • flushPaint, which calls the paint method of the RenderObject that needs to be redrawn and converts it into a Layer tree. The paint here will also call different paint methods according to the type of RenderObject, and finally call the canvas to realize the interface drawing.

  • CompositeFrame, according to the Layertree drawn by the canvas, calls the layer.buildScene method to convert the Layertree into scene information, and finally calls the render method of the window to display the interface to the user.

The above is the description of the drawing process. Based on the above execution process, let’s analyze in detail which links in the coding process can improve the performance experience.

Performance optimization direction

There are two key processes in the above process, one is layout and the other is drawing. During the layout process, it will be judged whether re-layout is needed based on the execution result of the markNeedsLayout function, and the other is based on the result of markNeedsPaint to judge whether it needs to be redrawn. So in these two functions, what details should we pay attention to when coding?

markNeedsPaint

The markNeedsPaint in the process of Figure 4 is a very critical point. This mark will directly affect the execution performance of the final drawing function flushPaint. Let’s disassemble this function step by step:

void markNeedsPaint() {
  assert(owner == null || !owner.debugDoingPaint);
  if (_needsPaint)
    return;
  _needsPaint = true;
  /// ... more code
}

First judge whether _needsPaint has been marked as true, and exit directly if it is marked.

_needsPaint = true;
if (isRepaintBoundary) {
  assert(() {
    if (debugPrintMarkNeedsPaintStacks)
      debugPrintStack(label: 'markNeedsPaint() called for $this');
    return true;
  }());
  // If we always have our own layer, then we can just repaint
  // ourselves without involving any other nodes.
  assert(_layer is OffsetLayer);
  if (owner != null) {
    owner._nodesNeedingPaint.add(this);
    owner.requestVisualUpdate();
  }
}

Mark the _needsPaint of the RenderObject as true, and then judge whether it is isRepaintBoundary, so what is isRepaintBoundary?

There is such a component RepaintBoundary in Flutter, which has its own isRepaintBoundary property as true , and you can use RepaintBoundary to wrap other components. This component represents the component as an independent rendering module. In the above code, if the current value is isRepaintBoundary, add the current RenderObject to nodesNeedingPaint and return it.

else if (parent is RenderObject) {
  final RenderObject parent = this.parent as RenderObject;
  parent.markNeedsPaint();
  assert(parent == this.parent);
}

If it is not currently isRepaintBoundary , you need to search for the parent node layer by layer, and mark _needsPaint layer by layer, so that all parent nodes on the current node need to perform _needsPaint operations.

Therefore, here is a point for coding performance considerations. We can use RepaintBoundary to encapsulate components that frequently need to be redrawn to reduce the redrawing operations of the parent node caused by the drawing of the current node. Most of the basic components in Flutter are wrapped with RepaintBoundary, so if you simply modify some components, it will not cause the redrawing of the parent component, which will affect the performance experience.

markNeedsLayout

markNeedsLayout is mainly used to mark whether to re-layout, the logic inside is very similar to markNeedsPaint. There is also room for performance improvement. When a component needs frequent layout adjustments, such as components that need to frequently add or delete elements, or components that need to be resized frequently, using RelayoutBoundary to encapsulate will have a certain room for performance improvement.

If you are writing a basic component yourself, you must pay great attention to this point. For some frequently changed points, or components that require frequent layout modifications, use RepaintBoundary and RelayoutBoundary for encapsulation. Secondly, when you analyze performance, pay special attention to these two key points. When there is a performance problem, you can try to find it from these two points.

Summary

In this class, from the rendering principle of the operating system, it analyzes why Flutter is superior to other cross-end technical frameworks in terms of performance experience. Next, it focuses on the core rendering principle of Flutter, and analyzes the performance optimization direction that needs to be paid attention to in the coding process from the rendering principle. After finishing this lesson, you need to master the core rendering process of Flutter, and master the use of RepaintBoundary and RelayoutBoundary in the coding process.

Click this link to view the source code of this lesson