Widget、Element、BuildContext 和 RenderObject
Widget
Widget
The inheritance relationship of key classes and their subclasses is shown in the figure:
Among them, Widget
is Widget Tree
the base class of all nodes. Widget
The subclasses are mainly divided into 3 categories:
-
The first category is
RenderObjectWidget
the subcategory of , specifically, it is divided intoSingleChildRenderObjectWidget
(single-child node container),LeafRenderObjectWidget
(leaf node),MultiChildRenderObjectWidget
(multi-child node container), and their common feature is that they all correspond to aRenderObject
subcategory of , which can carry out logic such asLayout
,Paint
. -
The second category is
StatelessWidget
andStatefulWidget
, which are the most commonly used by developersWidget
. They do not have the ability to draw themselves (that is, they do not correspondRender Object
), but they can be organized andRenderObjectWidget
configuredWidget
. -
Category 3 is
ProxyWidget
, specifically, subdivided intoParentDataWidget
andInheritedWidget
, which are characterized by providing additional data for their child nodes.
Element
Element
The key class and its subclass inheritance relationship are shown in the figure:
From the inheritance relationship that can be clearly seen in Figure 5-2 Element
, it implements BuildContext
the interface. Figure 5-2 corresponds to Figure 5-1, and each Element
has a corresponding one Widget
. Element
There are two direct subclasses ComponentElement
and RenderObjectElement
, of which ComponentElement
the two subclasses StatelessElement
and StatefulElement
correspond to StatelessWidget
and respectively StatefulWidget
.
We know that the final UI tree is actually composed of individual Element
nodes. The final layout and rendering of the component are all RenderObject
done through the pass. The general process from creation to rendering is: according to Widget
the generation Element
, then create the corresponding one RenderObject
and associate it with Element.renderObject
the attribute, and finally RenderObject
complete the layout arrangement and drawing through pass.
Element
It is Widget
an instantiated object at a specific location in the UI tree, most of which Element
are unique renderObject
, but some Element
have multiple child nodes, such as RenderObjectElement
some classes inherited from, for example MultiChildRenderObjectElement
. In the end, all Element
of them RenderObject
constitute a tree, which we call " Render Tree " or " render tree ".
To sum up, we can think that Flutter's UI system contains three trees: Widget tree , Element tree , and rendering tree . Their dependencies are: the Element tree is generated based on the Widget tree, and the rendering tree depends on the Element tree , as shown in the figure.
Now let's focus on Element
the Element
life cycle of the following:
-
Framework calls to
Widget.createElement
create anElement
instance, denoted aselement
-
Framework call
element.mount(parentElement,newSlot)
,mount
in the method, first call theelement
corresponding method to create the object associated with it , and then call the method to add it to the position specified by the slot in the rendering tree (this step is not necessary, it usually needs to be recreated when the tree structure changes Add to). After being inserted into the rendering tree, it is in the " " state, and it can be displayed on the screen (can be hidden) after being in the " " state.Widget
createRenderObject
element
RenderObject
element.attachRenderObject
element.renderObject
Element
element
active
active
-
When the configuration data of the parent
Widget
changes, and theState.build
returnedWidget
structure is different from the previous one, the correspondingElement
tree needs to be rebuilt. In order toElement
reuse,Element
before rebuilding, it will try to reuse the same position on the old treeelement
. The node will call its corresponding methodelement
before updating. If it returns , the old one will be reused , and the old one will be updated with the new configuration data. , otherwise a new one will be created .Widget
canUpdate
true
Element
Element
Widget
Element
Widget.canUpdate
The main thing is to judgenewWidget
whether the sumoldWidget
ofruntimeType
andkey
is equal at the same time, and return if they are equal at the same timetrue
, otherwise it will returnfalse
. According to this principle, when we need to force an update , we can avoid multiplexingWidget
by specifying a different one .Key
-
When an ancestor
Element
decides to removeelement
(for example,Widget
the tree structure has changed, resulting inelement
the correspondingWidget
being removed), then the ancestorElement
will call the method to remove it, and it will also be removed from the rendering treedeactivateChild
after removal .element.renderObject
Then Framework will callelement.deactivate
the method, andelement
the status will change to "inactive
". -
"
inactive
" statuselement
will not be displayed on the screen again. In order to avoid repeatedly creating and removing a specific object during an animation executionelement
, the "inactive
" state will be retained until the end of the last frame of the current animation. If it has not returned to the " " stateelement
after the animation execution ends , Frameworkactive
Its method will be calledunmount
to remove it completely. At this time ,element
the state is thatdefunct
it will never be inserted into the tree again. -
If
element
you want to reinsert intoElement
another location of the tree, such as the ancestor ofelement
orelement
has oneGlobalKey
(for global reuse elements), then the Framework will firstelement
remove it from the existing location, then call itsactivate
method, andrenderObject
re-attach
enter the rendering tree .
Summarize:
- An Element object will initialize the state when it is created
initial
, andmount
become a state after being added to the Element Tree through a methodactive
; when the Widget corresponding to the node fails, it willdeactivate
enterinactive
the state through a method.Element
If other nodes havekey
reused the node during the Build process of the current frame, theactivate
method will be used to make the node enteractive
the state again; if the node is still not in the Element Tree after the end of the current frame, it willunmount
be uninstalled through the method. And enterdefunct
the state, waiting for the subsequent logic to be destroyed .
After reading Element
the life cycle, some people may have doubts, will the developer directly operate the Element tree?
In fact, for developers, in most cases, they only need to pay attention Widget
to the tree. The Flutter framework has mapped the operations on the Widget tree to Element
the tree, which can greatly reduce complexity and improve development efficiency.
But understanding Element
is crucial to understanding the entire Flutter UI framework. It is through Element
this link that Flutter connects Widget
and RenderObject
connects. Understanding the Element layer will not only help developers have a clear understanding of the Flutter UI framework, but also improve their own abstraction. capabilities and design capabilities. In addition, sometimes, we have to directly use the Element object to complete some operations, such as obtaining theme data.
BuildContext
We already know that the methods of StatelessWidget
and will pass an object:StatefulWidget
build
BuildContext
Widget build(BuildContext context) {
}
We also know that in many cases we need to use this context
to do something, such as:
Theme.of(context) // 获取主题
Navigator.push(context, route) // 入栈新路由
Localizations.of(context, type) // 获取Local
context.size // 获取上下文大小
context.findRenderObject() // 查找当前或最近的一个祖先RenderObject
So BuildContext
what is it? Check its definition and find that it is an abstract interface class:
abstract class BuildContext {
...
}
context
Who is the implementation class corresponding to this object? We followed the vine and found build
that the call occurred in StatelessWidget
the corresponding StatefulWidget
method , for example in :StatelessElement
StatefulElement
build
StatelessElement
class StatelessElement extends ComponentElement {
...
Widget build() => widget.build(this);
...
}
Also in StatefulElement
:
class StatefulElement extends ComponentElement {
...
Widget build() => state.build(this);
...
}
It is found that build
the parameters passed are this
, obviously! This BuildContext
is StatelessElement
or StatefulElement
itself. But StatelessElement
and StatefulElement
itself did not implement BuildContext
the interface. Continue to trace the code and find that they indirectly inherit from the Element
class. Then check Element
the class definition and find Element
that the class really implements BuildContext
the interface:
abstract class ComponentElement extends Element {
...}
abstract class Element extends DiagnosticableTree implements BuildContext {
...}
So far the truth is clear, BuildContext
it is widget
corresponding Element
, so we can directly access the object in the context
method of StatelessWidget
and . Inside the code where we get the subject data is the method called .StatefulWidget
build
Element
Theme.of(context)
Element
dependOnInheritedWidgetOfExactType()
Summary: BuildContext
It is Element
the deity, BuildContext
the method call of is the operation Element
, Widget
it is the coat, and Element
it is the naked body under the coat.
Another meaning of BuildContext
Another implication about BuildContext
is that it is a reference to a position Widget
in Widget
the tree, and it contains information about the position in the tree Widget
,Widget
not about Widget
itself.
In the case of topics, since each Widget
has its own BuildContext
, this means that if you have multiple topics spread out in the tree, getting a Widget
topic for one might return a Widget
different result than another. In the specific case of the topic in the counter application example program, or in other of
methods, you will get the closest parent node of that type in the tree .
Advanced
We can see that it is the link between the internal connection and Element
the Flutter UI framework . Most of the time, developers only need to pay attention to the layer, but sometimes the layer cannot completely shield the details, so the Framework passes the object to the Developers, in this way, developers can directly manipulate objects when needed.widget
RenderObject
widget
widget
Element
StatelessWidget
StatefulWidget
build
Element
Element
So now there are two questions:
1. If there is no widget layer, can a usable UI framework be built with the Element layer alone? If so what should it look like?
2. Can the Flutter UI framework not be responsive?
For question 1, the answer is of course yes, because we said before that widget
the tree is just Element
the mapping of the tree, it only provides configuration information describing the UI tree, Widget
that is, the coat, of course a person can live in shame without wearing clothes, but wearing Clothes will live a more decent life, even if Widget
we don't rely on it, we can Element
build a UI framework completely through it.
Here is an example:
Element
We simulate a function purely StatefulWidget
. Suppose there is a page with a button. The text of the button is a 9-digit number. Click the button once to randomly arrange the 9 numbers. The code is as follows:
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
Widget build() {
Color primary = Theme.of(this).primaryColor; //1
return GestureDetector(
child: Center(
child: TextButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); //点击后将该Element标记为dirty,Element将会rebuild
},
),
),
);
}
}
-
The above
build
method does not receive parameters, which is different from the method inStatelessWidget
andStatefulWidget
inbuild(BuildContext)
. The places that need to be used in the code can beBuildContext
directly replaced with . For example, the parameters in code comment 1 can be passed directly , because the current object itself is an instance.this
Theme.of(this)
this
Element
-
When
text
a change occurs, we callmarkNeedsBuild()
the methodElement
to mark the current onedirty
, and the marked onedirty
willElement
be rebuilt in the next frame. In fact,State.setState()
it is also the method called internallymarkNeedsBuild()
. -
The method in the above code
build
still returns onewidget
. This is because there is alreadywidget
this layer in the Flutter framework, and the component library is alreadywidget
provided in the form of . If all components in the Flutter framework are providedHomeView
in the form of the exampleElement
, then you can use pureElement
to build the UI.HomeView
The return value type of thebuild
method can beElement
.
If we need to run the above code in the existing Flutter framework, we still have to provide an "adapter" that widget
will be HomeView
integrated into the existing framework. The following CustomHome
is equivalent to the "adapter":
class CustomHome extends Widget {
Element createElement() {
return HomeView(this);
}
}
Now you can CustomHome
add it to widget
the tree, we create it in a new routing page, the final effect is as shown in the following figure:
Click the button and the button text will be sorted randomly.
For question 2, the answer is of course yes. The API provided by the Flutter engine is original and independent. This is similar to the API provided by the operating system. The design of the upper UI framework depends entirely on the designer. The UI framework can be designed as Android-style or iOS-style, but those things Google won't do anymore. So in theory we can do it, but it’s not necessary. This is because the idea of responsiveness itself is great. The reason why this question is raised is because it’s one thing to do it or not to do it, but it’s another to know whether it can be done. It can reflect the degree of our understanding of knowledge.
RenderObject
We said that each Element
corresponds to one RenderObject
, and we can Element.renderObject
get it through . And we also said RenderObject
that the main responsibilities are layout and drawing, all of which RenderObject
will form a rendering tree Render Tree. The following will focus on RenderObject
the role.
RenderObject
It is an object in the rendering tree. Its main function is to implement the event responsebuild
and the execution process except in the rendering pipeline ( build
the process is element
realized by ), including: layout, drawing, layer composition and on-screen .
RenderObject
The key class and its subclasses are shown in Figure 5-3, and each of its subclasses corresponds to a RenderObjectWidget
type of Widget
node.
RenderView
Is a specialRenderObject
, is the root node of the entire Render Tree .- Another special thing
RenderObject
isRenderAbstractViewport
that it is an abstract class.RenderViewport
implements its interface, and indirectly inherits fromRenderBox
. RenderBox
andRenderSliver
are the most common in FlutterRenderObject
,RenderBox
responsible for the general layout of rows, columns, etc., andRenderSliver
responsible for the layout of each in the listItem
.
RenderObject
It has one parent
and one parentData
attributes, parent
pointing to its own parent node in the rendering tree, parentData
but a reserved variable. During the layout process of the parent component, it will determine the layout information of all its child components (such as position information, that is, the offset relative to the parent component ), and these layout information need to be saved in the layout phase, because the layout information needs to be used in the subsequent drawing phase (to determine the drawing position of the component), and the main function of the attribute is to save the layout information, such as parentData
in Stack
the layout, RenderStack
The offset data of the child element will be stored in the child element parentData
(see Positioned
the implementation for details).
Question: Now that there are RenderObject
, why does the Flutter framework provide RenderBox
and RenderSliver
two subclasses?
-
This is because
RenderObject
the class itself implements a set of basic layout and drawing protocols, but it does not define the child node model (for example, how many child nodes can a node have?), nor does it define the coordinate system (for example, child node positioning is in the flute Carl coordinates or polar coordinates?) and specific layout protocol (whether through width and height or through constraint and size?, or whether the parent node sets the size and position of the child node before or after the layout of the child node, etc.). -
To this end, the Flutter framework provides a
RenderBox
and aRenderSliver
class, they are all inherited fromRenderObject
, the layout coordinate system adopts the Cartesian coordinate system, and the screen(top, left)
is the origin. Based on these two classes, Flutter implements the box modelRenderBox
layout based on and the on-demand loading model based on .Sliver
Start the process (root node build process)
Flutter Engine is based on the Dart operating environment, namely Dart Runtime. The key process of starting Dart Runtime is as follows:
Among them, Dart Runtime will first create and start DartVM
a virtual machine, and DartVM
after startup, it will initialize one DartIsolate
, and then start it, and at the end of the startup process, it will execute the entry method DartIsolate
of the Dart application . That is, the function of main
" " in our daily development :lib/main.dart
main()
void main() => runApp(MyApp());
You can see main()
that the function only calls one runApp()
method, let's see runApp()
what is done in the method:
void runApp(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}
The parameter here app
is one widget
, which is the Widget passed to the Flutter framework by our developers. It is the first component to be displayed after the Flutter application starts, and it WidgetsFlutterBinding
is the bridge that binds widget
the framework and the Flutter engine. It is defined as follows:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding._instance == null) {
WidgetsFlutterBinding();
}
return WidgetsBinding.instance;
}
}
First look at WidgetsFlutterBinding
the inheritance relationship of , we find that WidgetsFlutterBinding
it inherits from BindingBase
and mixes in many Binding
classes, so when it starts, it will trigger the constructors of these classes in the order of mixin.
GestureBinding
: Responsible for gesture processing, provideswindow.onPointerDataPacket
callbacks, binds the Framework gesture subsystem, and is the binding entry for the Framework event model and underlying events.ServicesBinding
: Responsible for providing platform-related capabilities, providingwindow.onPlatformMessage
callbacks for binding platform message channels (message channels), mainly handling native and Flutter communications.SchedulerBinding
: Responsible for the management of various callbacks in the rendering process, providingwindow.onBeginFrame
andwindow.onDrawFrame
callbacks, monitoring refresh events, and binding the Framework drawing scheduling subsystem.PaintingBinding
: Responsible for drawing related logic, binding drawing library, mainly used to process image cache.SemanticsBinding
: Responsible for providing accessibility, the bridge between the semantic layer and the Flutter engine, mainly the underlying support for auxiliary functions.RendererBinding
: Responsible for the final rendering of the Render Tree, holdingPipelineOwner
objects, and providing callbacks such aswindow.onMetricsChanged
, etc.window.onTextScaleFactorChanged
It is a bridge between the render tree and the Flutter engine.WidgetsBinding
: Responsible for the management of the three trees of Flutter, holdBuilderOwner
objects, and provide callbacks such aswindow.onLocaleChanged
, etc.onBuildScheduled
It is the bridge between the Flutter widget layer and the engine.
Before we understand why these are mixed in, Binding
let's introduce it first Window
, Window
which is the interface that Flutter Framework connects to the host operating system. Let's take a look at Window
part of the class definition:
class Window {
// 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
// DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
double get devicePixelRatio => _devicePixelRatio;
// Flutter UI绘制区域的大小
Size get physicalSize => _physicalSize;
// 当前系统默认的语言Locale
Locale get locale;
// 当前系统字体缩放比例。
double get textScaleFactor => _textScaleFactor;
// 当绘制区域大小改变回调
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale发生变化回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系统字体缩放变化回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
FrameCallback get onBeginFrame => _onBeginFrame;
// 绘制回调
VoidCallback get onDrawFrame => _onDrawFrame;
// 点击或指针事件回调
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
// 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
// 此方法会直接调用Flutter engine的Window_scheduleFrame方法
void scheduleFrame() native 'Window_scheduleFrame';
// 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
void render(Scene scene) native 'Window_render';
// 发送平台消息
void sendPlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) ;
// 平台通道消息处理回调
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
... //其他属性及回调
}
You can see that Window
the class contains some information about the current device and system, as well as some callbacks of the Flutter Engine.
Now let's go back and look at WidgetsFlutterBinding
the various mixes Binding
. By looking at these Binding
source codes, we can find that these Binding
are basically listening to and processing Window
some events of objects, and then packaging, abstracting and distributing these events according to the Framework model. It can be seen WidgetsFlutterBinding
that it is the "glue" that binds the Flutter Engine and the upper Framework. WidgetsFlutterBinding
The essence of is one WidgetsBinding
, and it has no special logic itself, so binding
it gains additional capabilities by mixing these classes.
The method WidgetsFlutterBinding.ensureInitialized()
is mainly responsible for initializing a WidgetsBinding
global singleton and returning WidgetsBinding
the singleton object, except that it does not do anything else. This also shows that it is just an adhesive standing on the shoulders of everyone.
Going back to runApp
the method, WidgetsBinding
after obtaining the singleton object, the method will be called immediately WidgetsBinding
and the method scheduleAttachRootWidget
is called in it attachRootWidget
, the code is as follows:
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget); }); // 注意,不是立即执行
}
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = rootElement == null;
_readyToProduceFrames = true; // 开始生成 Element Tree
_rootElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView, // Render Tree的根节点
debugShortDescription: '[root]',
child: rootWidget, // 开发者通过runApp传入Widget Tree的根节点
).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance.ensureVisualUpdate(); // 请求渲染
}
}
The above logic is the entry point for driving Element Tree
and Render Tree
creating. It should be noted that attachRootWidget
it is started by Timer.run
starting. This is to ensure that all logic is under the management of the message loop.
attachRootWidget
The method is mainly responsible for Widget
adding the root to RenderView
the top. Note that there are two variables in the code, renderView
one renderViewElement
, renderView
which RenderObject
is the root of the rendering tree, renderViewElement
but renderView
the corresponding Element
object. It can be seen that this method mainly completes the entire association from root widget
to root and RenderObject
then to root Element
process.
attachToRenderTree
The method will drive Element Tree
the build and return its root node. The source code implementation is as follows:
RenderObjectToWidgetElement<T> attachToRenderTree(
BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
// 首帧构建,element参数为空
owner.lockState(() {
element = createElement(); // 创建Widget对应的Element
element!.assignOwner(owner); // 绑定BuildOwner
});
owner.buildScope(element!, () {
// 开始子节点的解析与挂载
element!.mount(null, null);
});
} else {
// 如热重载等场景
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
This method is responsible for creating the root element
, ie RenderObjectToWidgetElement
, and will be associated element
with , ie create a tree corresponding to the tree. If has already been created, set the associated in the root to a new one, so it can be seen that will only be created once and will be reused later. Since the parameter of the first frame is , the creation is first completed through the method, and then bound to the instance of , so what is it? In fact, it is the management class of the framework, which tracks which needs to be rebuilt. This object will drive updates later .widget
widget
element
element
element
widget
element
element
null
createElement
BuildOwner
BuildOwner
widget
widget
Element Tree
After the construction of the three trees is completed, the logic attachRootWidget
in will be triggered ensureVisualUpdate
:
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle: // 闲置阶段,没有需要渲染的帧
// 计算注册到本次帧渲染的一次性高优先级回调,通常是与动画相关的计算
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks: // 处理Dart中的微任务
// 计算待渲染帧的数据,包括Build、Layout、Paint等流程,这部分内容后面将详细介绍
case SchedulerPhase.midFrameMicrotasks:
// 帧渲染的逻辑结束,处理注册到本次帧渲染的一次性低优先级回调
case SchedulerPhase.persistentCallbacks:
return;
}
}
The above logic will judge whether a frame rendering needs to be initiated according to the current stage. The state transition of each stage is shown in Figure 5-8.
In Figure 5-8, first, if there is no external (such as setState
method) and internal (such as animation heartbeat, listener for image loading completion), the Framework will be in the default idle
state. If there is a new frame data request for rendering, the Framework will handleBeginFrame
enter the state in the method driven by the Engine transientCallbacks
, mainly to handle high-priority one-time callbacks, such as animation calculations. After the above logic is completed, the Framework will update its status to midFrameMicrotasks
, and the specific microtask processing is driven by the Engine. Secondly, the Engine will call handleDrawFrame
the method, and the Framework will update the state at this time persistentCallbacks
to indicate that it will process the logic that must be executed for each frame, mainly related to the rendering pipeline. After completing the logic related to the rendering pipeline in the Framework, the Framework will update its own state postFrameCallbacks
and handle low-priority one-time callbacks (usually registered by developers or upper-level logic). Finally, Framework resets the state to idle
. idle
Is the final state of the Framework, and a state loop will only start when a frame rendering is required.
scheduleFrame
The logic of the method is as follows, it will initiate a request through the interface to render when platformDispatcher.scheduleFrame
the next signal arrives.Vsync
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) return;
ensureFrameCallbacksRegistered();
platformDispatcher.scheduleFrame();
_hasScheduledFrame = true;
}
Back runApp
to the implementation, after the component tree is built (build), when the call attachRootWidget
is complete , the last step will call the WidgetsFlutterBinding
instance scheduleWarmUpFrame()
method, which is implemented in SchedulerBinding
, and it will be drawn immediately after it is called. Before the drawing ends, this method will lock the event distribution, that is to say, Flutter will not respond to various events until the drawing is completed, which can ensure that no new redrawing will be triggered during the drawing process. scheduleWarmUpFrame
The code of the method is as follows:
// flutter/packages/flutter/lib/src/scheduler/binding.dart
void scheduleWarmUpFrame() {
if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle) return; // 已发送帧渲染请求
_warmUpFrame = true;
Timeline.startSync('Warm-up frame');
final bool hadScheduledFrame = _hasScheduledFrame;
Timer.run(() {
// 第1步,动画等相关逻辑
handleBeginFrame(null);
});
Timer.run(() {
// 第2步,立即渲染一帧(通常是首帧)
handleDrawFrame();
resetEpoch();
_warmUpFrame = false; // 首帧渲染完成
if (hadScheduledFrame) scheduleFrame();
});
lockEvents(() async {
// 第3步,首帧渲染前不消费手势
await endOfFrame;
Timeline.finishSync();
});
}
The above logic is mainly divided into 3 steps, but it should be noted that the third step is executed first, because the first two steps are Timer.run
started in the method. handleBeginFrame
The method will trigger animation-related logic, and handleDrawFrame
the method will trigger the update of the 3 trees and rendering logic such Render Tree
as Layout and Paint . Normally, these two logics are driven by the Engine by listening to the Vsync signal. The reason for direct execution here is to ensure that the first frame is rendered as soon as possible, because no matter when the Vsync signal arrives, the first frame must be rendered.
Summarize
rendering pipeline
In the previous analysis , after the initialization process triggered by runApp
the method is executed , two methods will be triggered. The former is responsible for the generation of the Render Tree, and the latter is responsible for the triggering of the first frame rendering .ensureInitialized
scheduleAttachRootWidget
scheduleWarmUpFrame
1. Frame
A drawing process, we call it a frame (frame). What we said before that Flutter can achieve 60fps (Frame Per-Second) means that it can trigger up to 60 redraws per second. The larger the FPS value, the smoother the interface. What needs to be explained here is that the frame concept in Flutter is not equivalent to the screen refresh frame (frame), because the frame of the Flutter UI framework is not triggered every time the screen is refreshed, because if the UI does not change for a period of time, then every It is unnecessary to go through the rendering process every time the screen is refreshed. Therefore, Flutter will take an active request frame after the first frame is rendered to realize that the rendering process will be re-run only when the UI may change.
onBeginFrame
Flutter registers a and aonDrawFrame
callback on the window ,onDrawFrame
which will eventually be called in the callbackdrawFrame
.- After we call the method, the Flutter engine will call and
window.scheduleFrame()
at the right time (it can be considered as before the next refresh of the screen, depending on the implementation of the Flutter engine) .onBeginFrame
onDrawFrame
It can be seen that only active calls scheduleFrame()
will be executed drawFrame
. Therefore, when we mention in Flutter frame
, unless otherwise specified, it drawFrame()
corresponds to the call of , not to the refresh rate of the screen.
2. Flutter scheduling process SchedulerPhase
The execution process of the Flutter application is simply divided into two states idle
and . The state means that there is no processing. If the application state changes and the UI needs to be refreshed, you need to request a new one . When it arrives, it enters the state. The entire Flutter application life cycle is Toggles between and .frame
idle
frame
scheduleFrame()
frame
frame
frame
idle
frame
frame processing flow
When a new frame
arrives, the specific process is to execute the four task queues in sequence: transientCallbacks、midFrameMicrotasks、persistentCallbacks、postFrameCallbacks
, when the four task queues are executed, the current frame
ends. In summary, Flutter divides the entire life cycle into five states, and SchedulerPhase
expresses them through enumeration classes:
enum SchedulerPhase {
/// 空闲状态,并没有 frame 在处理。这种状态代表页面未发生变化,并不需要重新渲染。
/// 如果页面发生变化,需要调用`scheduleFrame()`来请求 frame。
/// 注意,空闲状态只是指没有 frame 在处理,通常微任务、定时器回调或者用户事件回调都
/// 可能被执行,比如监听了tap事件,用户点击后我们 onTap 回调就是在idle阶段被执行的。
idle,
/// 执行”临时“回调任务,”临时“回调任务只能被执行一次,执行后会被移出”临时“任务队列。
/// 典型的代表就是动画回调会在该阶段执行。
transientCallbacks,
/// 在执行临时任务时可能会产生一些新的微任务,比如在执行第一个临时任务时创建了一个
/// Future,且这个 Future 在所有临时任务执行完毕前就已经 resolve 了,这中情况
/// Future 的回调将在[midFrameMicrotasks]阶段执行
midFrameMicrotasks,
/// 执行一些持久的任务(每一个frame都要执行的任务),比如渲染管线(构建、布局、绘制)
/// 就是在该任务队列中执行的.
persistentCallbacks,
/// 在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和
/// 请求新的 frame。
postFrameCallbacks,
}
3. Rendering pipeline
When a new frame
arrives, the method WidgetsBinding
of is called drawFrame()
, let's take a look at its implementation:
void drawFrame() {
...//省略无关代码
try {
buildOwner.buildScope(renderViewElement); // 先执行构建
super.drawFrame(); //然后调用父类的 drawFrame 方法
}
}
In fact, the key code is only two lines: first rebuild ( build
), and then call drawFrame
the method of the parent class. After we drawFrame
expand the method of the parent class:
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
//下面是 展开 super.drawFrame() 方法
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息
pipelineOwner.flushPaint(); // 4.重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
...
}
}
You can see that there are mainly 5 things done:
- Rebuild the widget tree.
- Update the layout.
- Update Layer Composition information.
- redraw.
- Upper screen: display the drawn product on the screen.
We call the above 5 steps rendering pipeline
, the Chinese translation is "rendering pipeline" or "rendering pipeline".
Any UI framework, whether it is Web or Android, will have its own rendering pipeline. The rendering pipeline is the core of the UI framework, responsible for processing user input, generating UI description, rasterizing drawing instructions, and final data on the screen. Flutter is no exception. Due to the self-rendering method, Flutter's rendering pipeline is independent of the platform . Taking Android as an example, Flutter just Embedder
obtains one Surface
or Texture
serves as the final output target of its own rendering pipeline.
Flutter's rendering pipeline needs to be driven by the Vsync signal from the system. When the UI needs to be updated, the Framework will notify the Engine , and the Engine will wait until the next Vsync
signal arrives, then notify the Framework to animate, build, layout, paint , and finally generate layer
Submit to Engine . The Engine will layer
combine to generate textures, and finally submit the data to the GPU through the Open GL interface , and the GPU will display it on the monitor after processing, as shown in the figure below:
Specifically, Flutter's rendering pipeline is divided into the following seven steps.
-
(1) User Input (User Input) : Respond to the gesture behavior generated by the user through the mouse, keyboard, touch screen and other devices.
-
(2) Animation (Animation) : Update the data of the current frame based on the timer (Timer).
-
(3) Build (Build) : the phases of creating, updating and destroying the three trees,
StatelessWidget
and the methodsState
of andbuild
will be executed in this phase. -
(4) Layout :
Render Tree
The calculation of the size and position of each node will be completed at this stage. -
(5) Paint : The method of
Render Tree
traversing each node and generatingLayer Tree
will be executed at this stage to generate a series of drawing instructions.RenderObject
paint
-
(6) Composition : Processing
Layer Tree
to generate anScene
object as input for rasterization. -
(7) Rasterize : process drawing instructions into raw data that can be screened on the GPU .
Let's take setState
the update execution process of , as an example, to have a general understanding of the entire update process.
setState execution flow
When setState
called:
- First call the current
element
methodmarkNeedsBuild
to mark theelement
current as ._dirty
true
- Subsequent calls add
scheduleBuildFor
the current one to the list of .element
BuildOwner
_dirtyElements
- At the same time a new one will be requested
frame
, which will then be drawnframe
:onBuildScheduled->ensureVisualUpdate->scheduleFrame()
.
The following is setState
a rough flowchart of the execution:
The logic of which updateChild()
is as follows:
Among them onBuildScheduled
, the method is initialized in the startup phase, and it will eventually be called ensureVisualUpdate
, which will trigger the monitoring of the Vsync signal. When the new Vsync signal arrives, buildScope
the method will be triggered, which will rebuild the subtree and execute the rendering pipeline process at the same time:
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); //重新构建widget树
pipelineOwner.flushLayout(); // 更新布局
pipelineOwner.flushCompositingBits(); //更新合成信息
pipelineOwner.flushPaint(); // 更新绘制
if (sendFramesToEngine) {
renderView.compositeFrame(); // 上屏,会将绘制出的bit数据发送给GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
-
Rebuild
widget
the tree: IfdirtyElements
the list is not empty, traverse the list and call eachelement
methodrebuild
to rebuild the newwidget
(tree), since the newwidget
(tree) is built with a new state, it may causewidget
layout information (occupied space and position) changes, if it changes, its method will be calledrenderObject
,markNeedsLayout
which will search from the current node to the parent until it finds arelayoutBoundary
node, and then add it to a globalnodesNeedingLayout
list; if the root node is also If not foundrelayoutBoundary
, the root node is added tonodesNeedingLayout
the list. -
Update layout: Traverse the array, re-layout (call its method)
nodesNeedingLayout
for each one , and determine the new size and offset. It will be called in the method , which is similar to the function of the method. It will also search from the current node to the parent until it finds a parent node whose property is , and then add it to a global list; because the root node ( ) is , So one must be found. After the search process is over , the method will be called , and the method will be called eventually. In this method, it will first determine whether a new one has been requested , and if not, request a new one .renderObject
layout
layout
markNeedsPaint()
markNeedsLayout
isRepaintBoundary
true
nodesNeedingPaint
RenderView
isRepaintBoundary
true
buildOwner.requestVisualUpdate
scheduleFrame()
frame
frame
-
Update composition information: ignore for now.
-
Update drawing: Traverse
nodesNeedingPaint
the list, call the method of each nodepaint
to redraw, and the drawing process will be generatedLayer
. It needs to be explained that the drawing results in flutter are stored inLayer
, that is to say, as long as they areLayer
not released, the drawing results will be cached. Therefore, the drawing resultsLayer
canframe
be cached across to avoid unnecessary redrawing overhead. During the drawing process of the Flutter framework, when a nodeisRepaintBoundary
with is encounteredtrue
, a new one will be generatedLayer
. It can be seen that there is not a one-to-one correspondence betweenLayer
andrenderObject
, and the parent-child nodes can be shared, which we will verify in a subsequent experiment. Of course, if it is a custom component, we can manually add any number of Layers in the renderObject. This is usually used for caching scenes of drawing elements that only need to be drawn once and will not change later. We will also use an example to explain this later demo. -
Upper screen: After the drawing is completed, what we get is a
Layer
tree , and finally we need toLayer
display the drawing information in the tree on the screen. We know that Flutter is a self-implemented rendering engine, so we need to submit the drawing information to the Flutter engine, andrenderView.compositeFrame
this mission is accomplished.
The above is setState
the approximate update process from the call to the UI update. The actual process will be more complicated. For example, build
it is not allowed to call again during the process setState
, and the framework needs to do some checks. Another example is that frame
it involves the scheduling of animations, and when it is on the screen, all the animations will be Layer
added to the scene (Scene) object, and then the scene will be rendered.
setState execution timing problem
setState
will be triggered build
, but executed build
in the execution phase, so as long as it is not executed in this phase , it is absolutely safe, but this kind of granularity is too coarse, for example, in the and phases, if the application state changes, the best way is to only mark the component as , instead of requesting a new one , because the current has not yet been executed , so the UI will be refreshed in the current frame rendering pipeline after it is executed later. Therefore, after the marking is completed, the scheduling status will be judged first, and a new one will be requested only if it is or in the execution phase :persistentCallbacks
setState
transientCallbacks
midFrameMicrotasks
dirty
frame
frame
persistentCallbacks
setState
dirty
idle
postFrameCallbacks
frame
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame(); // 请求新的frame
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks: // 注意这一行
return;
}
}
The above code is fine in most cases, but if we build
call again in the stage, setState
there will still be problems, because if we build
call again in the stage setState
, it will cause build
... This will lead to a circular call, so the flutter framework finds that in build
If the stage is called, setState
an error will be reported, such as:
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, c) {
// build 阶段不能调用 setState, 会报错
setState(() {
++index;
});
return Text('xx');
},
);
}
After running, an error will be reported, and the console will print:
==== Exception caught by widgets library ====
The following assertion was thrown building LayoutBuilder:
setState() or markNeedsBuild() called during build.
It should be noted that if we build
call directly in setState
, the code is as follows:
Widget build(BuildContext context) {
setState(() {
++index;
});
return Text('$index');
}
No error will be reported after running. The reason is that the state build
of the current component dirty
(corresponding to middle element
) is during execution true
, and build
it will be set to only after execution false
. When setState
executing, it will first judge the current dirty
value, and if it is true
, it will return directly, so no error will be reported.
Above we only discussed build
that calling in the phase setState
will cause errors. In fact, it cannot be called synchronously in the entire construction, layout and drawing phases setState
. This is because calling in these phases setState
may request new ones frame
, which may lead to cyclic calls. If you want to update the application state during these stages, you cannot call it directly setState
.
security update
Now we know that build
it cannot be called in the phase setState
. In fact, it is not possible to directly request re-layout or redrawing synchronously during the layout phase and the drawing phase of the component. The reason is the same. What is the correct update method in these phases? We Taking setState
as an example , you can use the following methods:
// 在build、布局、绘制阶段安全更新
void update(VoidCallback fn) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
}
Note that update
the function should only be executed when frame
executing persistentCallbacks
, other stages setState
can be called directly. Because idle
the state will be a special case, if idle
it is called in the state update
, you need to manually call to scheduleFrame()
request a new one frame
, otherwise it will not be executed until postFrameCallbacks
the next one frame
(requested by other components ) arrives, so we can modify it:frame
update
void update(VoidCallback fn) {
final schedulerPhase = SchedulerBinding.instance.schedulerPhase;
if (schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
} else {
setState(fn);
}
}
So far, we have encapsulated a update
function that can safely update the state.
Now let's recall that in the "Custom Component: CustomCheckbox" section, in order to perform animation, we request redrawing through the following code after drawing is completed:
SchedulerBinding.instance.addPostFrameCallback((_) {
...
markNeedsPaint();
});
We don't call it directly markNeedsPaint()
, and the reason is as mentioned above.
Summarize
It should be noted that the Build process and the Layout process can be executed alternately .
reference: