With the booming development of mobile smart devices, a mobile multi-terminal development framework has become a general trend. Download the Flutter Analysis and Practice: Evolution and Innovation of Xianyu Technologies eBook for step-by-step analyses based on real-life problems, as well as clear explanations of the important concepts of Flutter.
Xianyu tried to use DinamicX's domain specific language (DSL) to deliver dynamic templates and implement dynamic template rendering on the Flutter side. We thought it was just simple mapping and data binding from DSL to widgets, but the actual effect of the operation was very poor, with serious list lagging and frame losses. Therefore, we have to analyze the widget creation, layout, and rendering at the Flutter framework layer.
DSL-to-native solutions are frequently used during iOS and Android app development. On Android, we describe the page layout by writing XML files. Why is the native mapping scheme not feasible on Flutter?
Let's take a look at a simple example for DSL definition.
As shown in the preceding figure, the design of DSL is similar to an XML file on Android. The Width and Height attributes of each node in the DSL file can be set to match_parent
and match_content
, respectively.
match_parent
: the size of the current node. Make it as large as the parent node.match_content
: the size of the current node. Reduce it to the size of a child node.In Flutter, match_parent
and match_content
are unavailable. Initially, our idea was very simple. In the build method of widgets, if the attribute is match_parent
, traverse upward until we find a parent node with certain width and height values. If the attribute is match_content
, traverse all child nodes to obtain their sizes. If a child node has the match_content
attribute, we recursively call the child node.
Caching the width and height calculation on each node cannot realize a one-time linear layout, but the overhead is not very large. However, the widget is immutable and lightweight and only contains the view configuration information. In Flutter, widgets are frequently created and destroyed, which leads to frequent layout calculations.
To solve these problems, we need to deal with widgets and do more processing on the Element
and RenderObject
. This is why custom widgets are required.
Next, let's learn about the build-, layout-, and paint-related logic of widgets in Flutter according to the source code.
The following sections describe Widget
, Element
, and RenderObject
through a simple widget, Opacity
.
In Flutter, everything is a widget. A widget is immutable and lightweight and only contains the view configuration information. The overhead of creating and deleting widgets is small.
Opacity inherits from RenderObjectWidget
and defines two critical functions:
RenderObjectElement createElement();
RenderObject createRenderObject(BuildContext context);
Element
and RenderObject
– Here we define only the creation logic. The specific call time will be introduced later.
In SingleChildRenderObjectWidget
, you can see that a SingleChildRenderObjectElement
object has been created.
Element
is an abstraction of a widget. Widget.createElement
is called to create a widget. Element
holds Widget
and RenderObject
. BuildOwner
traverses the Element tree and builds a RenderObject
tree based on whether an Element
is marked as dirty. Element
concatenates Widget
and RenderObject
during the entire view build process.
The createRenderObject
function of Opacity creates the RenderOpacity
object and RenderObject
provides the data required for rendering for the engine layer. The Paint method of RenderOpacity
finds the target for rendering.
void paint(PaintingContext context, Offset offset)
{
if (child != null)
{
... context.pushOpacity(offset, _alpha, super.paint);
}
}
By using RenderObject
, we can handle layout, rendering, and hit testing. This is what we handle most in a custom widget. RenderObject
only defines the layout interface and does not implement the layout model. RenderBox
provides the definition of the BoxModel
protocol in the 2D Cartesian coordinate system. In most cases, RenderObject
can inherit from RenderBox
and implement a new layout, rendering, and tap event processing through reloading.
Flutter uses a one-time layout and the O(N) linear time for layout and rendering, as shown in Figure 3-14. During a traversal, a parent node calls the layout method of each child node and passes the constraints down. A child node calculates its own layout according to the constraints and passes the result back to the parent node.
Figure 3-14
If a node meets any of the following conditions, the node is marked as RelayoutBoundary
and the changes in the size of a child node do not affect the layout of its parent node:
parentUsesSize = false
: The layout of the parent node does not depend on the size of the current node.sizedByParent = true
: The size of the current node is determined by its parent node.constraints.isTight
: The value is fixed. The maximum width and height are the same as the minimum ones.parent is not RenderObject
: If the parent node is not RenderObject
, the parent node does not need to be informed of the changes in the child node layout.When the RelayoutBoundary
mark and the child node size changes, the parent node is not instructed to re-layout or render, as shown in Figure 3-15. This improves efficiency.
Figure 3-15
Why does frequent creation and destruction of widgets not affect rendering performance? Element
defines the updateChild
method. This method is called when Element
is created, Framework calls mount, and RenderObject
is marked as needsLayout
to execute RenderObject.performLayout
.
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
if (child != null) {
...
if (Widget.canUpdate(child.widget, newWidget)) {
...
child.update(newWidget);
...
}
}
}
If both child
and newWidget
have been set, you can use Widget.canUpdate
to determine whether the current child
element can be updated or reused.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
Widget.canUpdate
determines whether the current child element can be updated based on runtimeType
and key. If yes, the Element child node is updated. If no, Element of the child node is deactivated and a new Element is created based on newWidget
.
In the first version, all components inherit from Object
and a build method was implemented to set the widget properties based on the nodeData
converted by DSL, as shown in Figure 3-16.
Figure 3-16
For example, the first node has the match_content
attribute. Each time a widget is created, the layout needs to be calculated, as shown in Figure 3-17.
Figure 3-17
In this way, every time a widget is updated, the entire tree is traversed to calculate the size of the top node. What if a widget node is updated?
Figure 3-18
The calculation needs to be performed all over again because widgets are immutable and are frequently recreated and destroyed. In the worst case, the number of calculations will reach O(N2). You can imagine what it would be for a long list.
In the second version, Widget
, Element
, and RenderObject
are customized. Figure 3-19 shows a class diagram of some components.
Figure 3-19
In the figure, the components within the dashed line box are the custom widgets, which are roughly divided into three types:
CustomSingleChildLayout
.FrameLayout
and LinearLayout
, which inherit from CustomMultiChildLayout
.ListLayout
and PageLayout
, which inherit from CustomScrollView
.In the custom RenderObject
, the tap events and the rendering method are directly processed by widget combinations.
@override
bool hitTestChildren(HitTestResult result, {Offset position}) { return child?.hitTest(result, position: position) ?? false; }
@override void paint(PaintingContext context, Offset offset) { if (child != null) context.paintChild(child, offset); }
1) How can we handle match_content
?
The width and height values of the current node are set to match_content
. You need to calculate the size of the child nodes before calculating the size of the current node.
To implement the custom RenderObject
, we need to rewrite the performLayout
method. The performLayout
method performs the following operations:
sizedByParent
parameter is set to false, you must set the size of the node.Take a child node as an example (such as Padding.) In RenderObject
, for a node with the match_content
attribute, when the child layout method is called, set parentUsesSize
to true and set the size according to child.size
.
In this way, when the size of the child node changes, the parent node is automatically marked as needsLayout
, and the layout and rendering of the current frame will be reset in the pipeline. This also causes performance loss.
@override
void performLayout() { assert(callback != null); invokeLayoutCallback(callback); if (child != null) { child.layout(constraints, parentUsesSize: true); size = constraints.constrain(child.size); } else { size = constraints.biggest; }
In the case of multiple child nodes, you can refer to the internal implementation of RenderSliverList
.
2) How can we handle match_parent
?
If the width and height of the current node are set to match_parent
, make it as large as the parent node. In this case, when constraints are passed down, the node size is known without calculation. RenderObject
provides the sizedByParent
attribute, which defaults to false. If the attribute is set to match_parent
, sizedByParent
of the current RenderObject
is set to true. In this way, when constraints are passed down, the child node size is known without layout calculation. This improves the performance.
In RenderObject
, when sizedByParent
is set to true, the performResize
method must be reloaded.
@override
void performResize() { size = constraints.biggest; }
In this case, do not set the size when reloading the performLayout
method.
After sizedByParent
is changed, be sure to call the markNeedsLayoutForSizedByParentChange
method to set the current node and its parent node to needsLayout
and recalculate and rerender the layout.
Figure 3-20 shows the calculation process with one widget rendered in the second version.
Figure 3-20
Similarly, in RenderObject
, the performLayout
method is used to pass constraints down to calculate the child node size and pass it up. The layout calculation of the entire tree can be completed through one traversal.
As shown in Figure 3-21, what happens if it's in the update scenario?
Figure 3-21
According to the preceding Element update process and the RelayoutBoundary
optimization of RenderObject
, when new widget attributes are changed, the current Element node can be updated without the need to rebuild the Element tree. After the optimization of RelayoutBoundary
, fewer layout calculations are required for RenderObject
.
After the optimization, the average frame rate of long list sliding is increased from 28 to about 50.
Issues still exist in the implementation of custom widgets. According to the preceding performLayout
implementation, parentUsesSize
is always set to true when the layout method of each child node is called. However, parentUsesSize
needs to be set to true only when the current node attribute is match_content
. The current processing is too simple to take advantage of the RelayoutBoundary
optimization. Therefore, each widget update leads to layout calculations of 2N times. This is one of the reasons why the frame rate is lower than a Flutter page.
Currently, we have implemented the mapping of DSL to widgets, which makes Flutter dynamic template rendering possible. DSL is an abstraction, while XML is only one of the options. We will not only continuously improve the performance but also improve the abstraction of the entire solution to support general DSL conversion. We will provide a set of universal solutions to better empower businesses through technologies.
The conversion from DSL to widgets is only one part of the process. In the closed loop from template edition, local verification, Alibaba Cloud Content Delivery Network (CDN) delivery, phased testing, to online monitoring, further optimizations are still needed.
Flutter Analysis and Practice: Design of the Statistical Framework
56 posts | 4 followers
FollowXianYu Tech - September 8, 2020
Alibaba Clouder - December 22, 2020
Alibaba Clouder - September 21, 2020
XianYu Tech - August 10, 2021
XianYu Tech - September 11, 2020
XianYu Tech - September 4, 2020
56 posts | 4 followers
FollowHelp enterprises build high-quality, stable mobile apps
Learn MoreAn enterprise-level continuous delivery tool.
Learn MoreProvides comprehensive quality assurance for the release of your apps.
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreMore Posts by XianYu Tech