Flutter Notes | Flutter Core Principles (2) Key Classes and Startup Process

Widget、Element、BuildContext 和 RenderObject

Widget

WidgetThe inheritance relationship of key classes and their subclasses is shown in the figure:

insert image description here

Among them, Widgetis Widget Treethe base class of all nodes. WidgetThe subclasses are mainly divided into 3 categories:

  • The first category is RenderObjectWidgetthe subcategory of , specifically, it is divided into SingleChildRenderObjectWidget(single-child node container), LeafRenderObjectWidget(leaf node), MultiChildRenderObjectWidget(multi-child node container), and their common feature is that they all correspond to a RenderObjectsubcategory of , which can carry out logic such as Layout, Paint.

  • The second category is StatelessWidgetand StatefulWidget, which are the most commonly used by developers Widget. They do not have the ability to draw themselves (that is, they do not correspond Render Object), but they can be organized and RenderObjectWidgetconfigured Widget.

  • Category 3 is ProxyWidget, specifically, subdivided into ParentDataWidgetand InheritedWidget, which are characterized by providing additional data for their child nodes.

Element

ElementThe key class and its subclass inheritance relationship are shown in the figure:

insert image description here
From the inheritance relationship that can be clearly seen in Figure 5-2 Element, it implements BuildContextthe interface. Figure 5-2 corresponds to Figure 5-1, and each Elementhas a corresponding one Widget. ElementThere are two direct subclasses ComponentElementand RenderObjectElement, of which ComponentElementthe two subclasses StatelessElementand StatefulElementcorrespond to StatelessWidgetand respectively StatefulWidget.

We know that the final UI tree is actually composed of individual Elementnodes. The final layout and rendering of the component are all RenderObjectdone through the pass. The general process from creation to rendering is: according to Widgetthe generation Element, then create the corresponding one RenderObjectand associate it with Element.renderObjectthe attribute, and finally RenderObjectcomplete the layout arrangement and drawing through pass.

ElementIt is Widgetan instantiated object at a specific location in the UI tree, most of which Elementare unique renderObject, but some Elementhave multiple child nodes, such as RenderObjectElementsome classes inherited from, for example MultiChildRenderObjectElement. In the end, all Elementof them RenderObjectconstitute 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.

insert image description here
Now let's focus on Elementthe Elementlife cycle of the following:

  1. Framework calls to Widget.createElementcreate an Elementinstance, denoted aselement

  2. Framework call element.mount(parentElement,newSlot), mountin the method, first call the elementcorresponding 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.WidgetcreateRenderObjectelementRenderObjectelement.attachRenderObjectelement.renderObjectElementelementactiveactive

  3. When the configuration data of the parent Widgetchanges, and the State.buildreturned Widgetstructure is different from the previous one, the corresponding Elementtree needs to be rebuilt. In order to Elementreuse, Elementbefore rebuilding, it will try to reuse the same position on the old tree element. The node will call its corresponding method elementbefore 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 .WidgetcanUpdatetrueElementElementWidgetElement

    Widget.canUpdateThe main thing is to judge newWidgetwhether the sum oldWidgetof runtimeTypeand keyis equal at the same time, and return if they are equal at the same time true, otherwise it will return false. According to this principle, when we need to force an update , we can avoid multiplexing Widgetby specifying a different one .Key

  4. When an ancestor Elementdecides to remove element(for example, Widgetthe tree structure has changed, resulting in elementthe corresponding Widgetbeing removed), then the ancestor Elementwill call the method to remove it, and it will also be removed from the rendering tree deactivateChildafter removal . element.renderObjectThen Framework will call element.deactivatethe method, and elementthe status will change to " inactive".

  5. " inactive" status elementwill not be displayed on the screen again. In order to avoid repeatedly creating and removing a specific object during an animation execution element, the " inactive" state will be retained until the end of the last frame of the current animation. If it has not returned to the " " state elementafter the animation execution ends , Framework activeIts method will be called unmountto remove it completely. At this time , elementthe state is that defunctit will never be inserted into the tree again.

  6. If elementyou want to reinsert into Elementanother location of the tree, such as the ancestor of elementor elementhas one GlobalKey(for global reuse elements), then the Framework will first elementremove it from the existing location, then call its activatemethod, and renderObjectre- attachenter the rendering tree .

insert image description here

Summarize:

  • An Element object will initialize the state when it is created initial, and mountbecome a state after being added to the Element Tree through a method active; when the Widget corresponding to the node fails, it will deactivateenter inactivethe state through a method. ElementIf other nodes have keyreused the node during the Build process of the current frame, the activatemethod will be used to make the node enter activethe state again; if the node is still not in the Element Tree after the end of the current frame, it will unmountbe uninstalled through the method. And enter defunctthe state, waiting for the subsequent logic to be destroyed .

After reading Elementthe 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 Widgetto the tree. The Flutter framework has mapped the operations on the Widget tree to Elementthe tree, which can greatly reduce complexity and improve development efficiency.

But understanding Elementis crucial to understanding the entire Flutter UI framework. It is through Elementthis link that Flutter connects Widgetand RenderObjectconnects. 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 StatelessWidgetand will pass an object:StatefulWidgetbuildBuildContext

Widget build(BuildContext context) {
    
    }

We also know that in many cases we need to use this contextto do something, such as:

Theme.of(context) // 获取主题
Navigator.push(context, route) // 入栈新路由
Localizations.of(context, type) // 获取Local
context.size // 获取上下文大小
context.findRenderObject() // 查找当前或最近的一个祖先RenderObject

So BuildContextwhat is it? Check its definition and find that it is an abstract interface class:

abstract class BuildContext {
    
    
    ...
}

contextWho is the implementation class corresponding to this object? We followed the vine and found buildthat the call occurred in StatelessWidgetthe corresponding StatefulWidgetmethod , for example in :StatelessElementStatefulElementbuildStatelessElement

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 buildthe parameters passed are this, obviously! This BuildContextis StatelessElementor StatefulElementitself. But StatelessElementand StatefulElementitself did not implement BuildContextthe interface. Continue to trace the code and find that they indirectly inherit from the Elementclass. Then check Elementthe class definition and find Elementthat the class really implements BuildContextthe interface:

abstract class ComponentElement extends Element {
    
    ...}
abstract class Element extends DiagnosticableTree implements BuildContext {
    
    ...}

So far the truth is clear, BuildContextit is widgetcorresponding Element, so we can directly access the object in the contextmethod of StatelessWidgetand . Inside the code where we get the subject data is the method called .StatefulWidgetbuildElementTheme.of(context)ElementdependOnInheritedWidgetOfExactType()

Summary: BuildContextIt is Elementthe deity, BuildContextthe method call of is the operation Element, Widgetit is the coat, and Elementit is the naked body under the coat.

Another meaning of BuildContext

Another implication about BuildContextis that it is a reference to a position Widgetin Widgetthe tree, and it contains information about the position in the tree Widget,Widget not about Widgetitself.

In the case of topics, since each Widgethas its own BuildContext, this means that if you have multiple topics spread out in the tree, getting a Widgettopic for one might return a Widgetdifferent result than another. In the specific case of the topic in the counter application example program, or in other ofmethods, you will get the closest parent node of that type in the tree .

insert image description here

Advanced

We can see that it is the link between the internal connection and Elementthe 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.widgetRenderObjectwidgetwidgetElementStatelessWidgetStatefulWidgetbuildElementElement

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 widgetthe tree is just Elementthe mapping of the tree, it only provides configuration information describing the UI tree, Widgetthat 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 Widgetwe don't rely on it, we can Elementbuild a UI framework completely through it.

Here is an example:

ElementWe 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 buildmethod does not receive parameters, which is different from the method in StatelessWidgetand StatefulWidgetin build(BuildContext). The places that need to be used in the code can be BuildContextdirectly replaced with . For example, the parameters in code comment 1 can be passed directly , because the current object itself is an instance.thisTheme.of(this)thisElement

  • When texta change occurs, we call markNeedsBuild()the method Elementto mark the current one dirty, and the marked one dirtywill Elementbe rebuilt in the next frame. In fact, State.setState()it is also the method called internally markNeedsBuild().

  • The method in the above code buildstill returns one widget. This is because there is already widgetthis layer in the Flutter framework, and the component library is already widgetprovided in the form of . If all components in the Flutter framework are provided HomeViewin the form of the example Element, then you can use pure Elementto build the UI. HomeViewThe return value type of the buildmethod can be Element.

If we need to run the above code in the existing Flutter framework, we still have to provide an "adapter" that widgetwill be HomeViewintegrated into the existing framework. The following CustomHomeis equivalent to the "adapter":

class CustomHome extends Widget {
    
    
  
  Element createElement() {
    
    
    return HomeView(this);
  }
}

Now you can CustomHomeadd it to widgetthe tree, we create it in a new routing page, the final effect is as shown in the following figure:

insert image description here
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 Elementcorresponds to one RenderObject, and we can Element.renderObjectget it through . And we also said RenderObjectthat the main responsibilities are layout and drawing, all of which RenderObjectwill form a rendering tree Render Tree. The following will focus on RenderObjectthe role.

RenderObjectIt 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 ( buildthe process is elementrealized by ), including: layout, drawing, layer composition and on-screen .

insert image description here

RenderObjectThe key class and its subclasses are shown in Figure 5-3, and each of its subclasses corresponds to a RenderObjectWidgettype of Widgetnode.

  • RenderViewIs a special RenderObject, is the root node of the entire Render Tree .
  • Another special thing RenderObjectis RenderAbstractViewportthat it is an abstract class. RenderViewportimplements its interface, and indirectly inherits from RenderBox.
  • RenderBoxand RenderSliverare the most common in Flutter RenderObject, RenderBoxresponsible for the general layout of rows, columns, etc., and RenderSliverresponsible for the layout of each in the list Item.

RenderObjectIt has one parentand one parentDataattributes, parentpointing to its own parent node in the rendering tree, parentDatabut 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 parentDatain Stackthe layout, RenderStackThe offset data of the child element will be stored in the child element parentData(see Positionedthe implementation for details).

Question: Now that there are RenderObject, why does the Flutter framework provide RenderBoxand RenderSlivertwo subclasses?

  • This is because RenderObjectthe 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 RenderBoxand a RenderSliverclass, they are all inherited from RenderObject, 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:

insert image description here

Among them, Dart Runtime will first create and start DartVMa virtual machine, and DartVMafter startup, it will initialize one DartIsolate, and then start it, and at the end of the startup process, it will execute the entry method DartIsolateof the Dart application . That is, the function of main" " in our daily development :lib/main.dartmain()

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 appis 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 WidgetsFlutterBindingis the bridge that binds widgetthe 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 WidgetsFlutterBindingthe inheritance relationship of , we find that WidgetsFlutterBindingit inherits from BindingBaseand mixes in many Bindingclasses, so when it starts, it will trigger the constructors of these classes in the order of mixin.

insert image description here

  • GestureBinding: Responsible for gesture processing, provides window.onPointerDataPacketcallbacks, 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, providing window.onPlatformMessagecallbacks 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, providing window.onBeginFrameand window.onDrawFramecallbacks, 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, holding PipelineOwnerobjects, and providing callbacks such as window.onMetricsChanged, etc. window.onTextScaleFactorChangedIt is a bridge between the render tree and the Flutter engine.
  • WidgetsBinding: Responsible for the management of the three trees of Flutter, hold BuilderOwnerobjects, and provide callbacks such as window.onLocaleChanged, etc. onBuildScheduledIt is the bridge between the Flutter widget layer and the engine.

Before we understand why these are mixed in, Bindinglet's introduce it first Window, Windowwhich is the interface that Flutter Framework connects to the host operating system. Let's take a look at Windowpart 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 Windowthe 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 WidgetsFlutterBindingthe various mixes Binding. By looking at these Bindingsource codes, we can find that these Bindingare basically listening to and processing Windowsome events of objects, and then packaging, abstracting and distributing these events according to the Framework model. It can be seen WidgetsFlutterBindingthat it is the "glue" that binds the Flutter Engine and the upper Framework. WidgetsFlutterBindingThe essence of is one WidgetsBinding, and it has no special logic itself, so bindingit gains additional capabilities by mixing these classes.

The method WidgetsFlutterBinding.ensureInitialized()is mainly responsible for initializing a WidgetsBindingglobal singleton and returning WidgetsBindingthe 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 runAppthe method, WidgetsBindingafter obtaining the singleton object, the method will be called immediately WidgetsBindingand the method scheduleAttachRootWidgetis 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 Treeand Render Treecreating. It should be noted that attachRootWidgetit is started by Timer.runstarting. This is to ensure that all logic is under the management of the message loop.

attachRootWidgetThe method is mainly responsible for Widgetadding the root to RenderViewthe top. Note that there are two variables in the code, renderViewone renderViewElement, renderViewwhich RenderObjectis the root of the rendering tree, renderViewElementbut renderViewthe corresponding Elementobject. It can be seen that this method mainly completes the entire association from root widgetto root and RenderObjectthen to root Elementprocess.

attachToRenderTreeThe method will drive Element Treethe 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 elementwith , 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 .widgetwidgetelementelementelementwidgetelementelementnullcreateElementBuildOwnerBuildOwnerwidgetwidgetElement Tree

After the construction of the three trees is completed, the logic attachRootWidgetin 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.
insert image description here

In Figure 5-8, first, if there is no external (such as setStatemethod) and internal (such as animation heartbeat, listener for image loading completion), the Framework will be in the default idlestate. If there is a new frame data request for rendering, the Framework will handleBeginFrameenter 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 handleDrawFramethe method, and the Framework will update the state at this time persistentCallbacksto 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 postFrameCallbacksand handle low-priority one-time callbacks (usually registered by developers or upper-level logic). Finally, Framework resets the state to idle. idleIs the final state of the Framework, and a state loop will only start when a frame rendering is required.

scheduleFrameThe logic of the method is as follows, it will initiate a request through the interface to render when platformDispatcher.scheduleFramethe next signal arrives.Vsync

void scheduleFrame() {
    
    
  if (_hasScheduledFrame || !framesEnabled) return;
  ensureFrameCallbacksRegistered();  
  platformDispatcher.scheduleFrame();
  _hasScheduledFrame = true;
}

Back runAppto the implementation, after the component tree is built (build), when the call attachRootWidgetis complete , the last step will call the WidgetsFlutterBindinginstance 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. scheduleWarmUpFrameThe 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.runstarted in the method. handleBeginFrameThe method will trigger animation-related logic, and handleDrawFramethe method will trigger the update of the 3 trees and rendering logic such Render Treeas 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

insert image description here

rendering pipeline

In the previous analysis , after the initialization process triggered by runAppthe 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 .ensureInitializedscheduleAttachRootWidgetscheduleWarmUpFrame

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.

  1. onBeginFrameFlutter registers a and a onDrawFramecallback on the window , onDrawFramewhich will eventually be called in the callback drawFrame.
  2. 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) .onBeginFrameonDrawFrame

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 idleand . 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 .frameidleframescheduleFrame()frameframeframeidleframe

frame processing flow

When a new framearrives, 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 frameends. In summary, Flutter divides the entire life cycle into five states, and SchedulerPhaseexpresses 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 framearrives, the method WidgetsBindingof 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 drawFramethe method of the parent class. After we drawFrameexpand 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:

  1. Rebuild the widget tree.
  2. Update the layout.
  3. Update Layer Composition information.
  4. redraw.
  5. 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 Embedderobtains one Surfaceor Textureserves as the final output target of its own rendering pipeline.

insert image description here

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 Vsyncsignal arrives, then notify the Framework to animate, build, layout, paint , and finally generate layerSubmit to Engine . The Engine will layercombine 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:

insert image description here

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, StatelessWidgetand the methods Stateof and buildwill be executed in this phase.

  • (4) Layout : Render TreeThe calculation of the size and position of each node will be completed at this stage.

  • (5) Paint : The method of Render Treetraversing each node and generating Layer Treewill be executed at this stage to generate a series of drawing instructions.RenderObjectpaint

  • (6) Composition : Processing Layer Treeto generate an Sceneobject as input for rasterization.

  • (7) Rasterize : process drawing instructions into raw data that can be screened on the GPU .

Let's take setStatethe update execution process of , as an example, to have a general understanding of the entire update process.

setState execution flow

When setStatecalled:

  • First call the current elementmethod markNeedsBuildto mark the elementcurrent as ._dirtytrue
  • Subsequent calls add scheduleBuildForthe current one to the list of .elementBuildOwner_dirtyElements
  • At the same time a new one will be requested frame, which will then be drawn frame: onBuildScheduled->ensureVisualUpdate->scheduleFrame().

The following is setStatea rough flowchart of the execution:

insert image description here

The logic of which updateChild()is as follows:

insert image description here

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, buildScopethe 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;
  }
}
  1. Rebuild widgetthe tree: If dirtyElementsthe list is not empty, traverse the list and call each elementmethod rebuildto rebuild the new widget(tree), since the new widget(tree) is built with a new state, it may cause widgetlayout information (occupied space and position) changes, if it changes, its method will be called renderObject, markNeedsLayoutwhich will search from the current node to the parent until it finds a relayoutBoundarynode, and then add it to a global nodesNeedingLayoutlist; if the root node is also If not found relayoutBoundary, the root node is added to nodesNeedingLayoutthe list.

  2. Update layout: Traverse the array, re-layout (call its method) nodesNeedingLayoutfor 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 .renderObjectlayoutlayoutmarkNeedsPaint()markNeedsLayoutisRepaintBoundarytruenodesNeedingPaintRenderViewisRepaintBoundarytruebuildOwner.requestVisualUpdatescheduleFrame()frameframe

  3. Update composition information: ignore for now.

  4. Update drawing: Traverse nodesNeedingPaintthe list, call the method of each node paintto redraw, and the drawing process will be generated Layer. It needs to be explained that the drawing results in flutter are stored in Layer, that is to say, as long as they are Layernot released, the drawing results will be cached. Therefore, the drawing results Layercan framebe cached across to avoid unnecessary redrawing overhead. During the drawing process of the Flutter framework, when a node isRepaintBoundarywith is encountered true, a new one will be generated Layer. It can be seen that there is not a one-to-one correspondence between Layerand renderObject, 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.

  5. Upper screen: After the drawing is completed, what we get is a Layertree , and finally we need to Layerdisplay 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, and renderView.compositeFramethis mission is accomplished.

The above is setStatethe approximate update process from the call to the UI update. The actual process will be more complicated. For example, buildit is not allowed to call again during the process setState, and the framework needs to do some checks. Another example is that frameit involves the scheduling of animations, and when it is on the screen, all the animations will be Layeradded to the scene (Scene) object, and then the scene will be rendered.

setState execution timing problem

setStatewill be triggered build, but executed buildin 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 :persistentCallbackssetStatetransientCallbacksmidFrameMicrotasksdirtyframeframepersistentCallbackssetStatedirtyidlepostFrameCallbacksframe

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 buildcall again in the stage, setStatethere will still be problems, because if we buildcall again in the stage setState, it will cause build... This will lead to a circular call, so the flutter framework finds that in buildIf the stage is called, setStatean 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 buildcall 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 buildof the current component dirty(corresponding to middle element) is during execution true, and buildit will be set to only after execution false. When setStateexecuting, it will first judge the current dirtyvalue, and if it is true, it will return directly, so no error will be reported.

Above we only discussed buildthat calling in the phase setStatewill 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 setStatemay 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 buildit 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 setStateas an example , you can use the following methods:

// 在build、布局、绘制阶段安全更新
void update(VoidCallback fn) {
    
    
  SchedulerBinding.instance.addPostFrameCallback((_) {
    
    
    setState(fn);
  });
}

Note that updatethe function should only be executed when frameexecuting persistentCallbacks, other stages setStatecan be called directly. Because idlethe state will be a special case, if idleit 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 postFrameCallbacksthe next one frame(requested by other components ) arrives, so we can modify it:frameupdate

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 updatefunction 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

insert image description here
It should be noted that the Build process and the Layout process can be executed alternately .


reference:

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/130931167