Fluent | start, render, setState process

Posted by Gordonator on Fri, 11 Feb 2022 23:20:16 +0100

preface

After using Flutter for so long, I don't even know his startup process. It's really a shame to learn. Today, let's analyze Flutter's startup process and his rendering process, and make a simple analysis of it.

Start process

The start-up entry of FLUENT is in lib / main In the main() function in Dart, it is the starting point of Dart application. The simplest implementation of the main function is as follows:

void main() => runApp(MyApp());
Copy code

You can see that only the runApp() method is called in the main function. Let's see what's done in it:

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}
Copy code

Received a widget parameter, which is the first component to be displayed after the start-up of the Flutter, and WidgetsFlutterBinding is the bridge that binds the widget and the Flutter engine. The definition is as follows:

///Specific binding of applications based on Widgets framework.
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance!;
  }
}
Copy code

We can see that WidgetsFlutterBinding inherits from BindingBase and is mixed with many bindings. Before introducing these bindings, let's introduce Window. The following is the official explanation of Window:

The most basic interface to the host operating system's user interface. The most basic interface of the host operating system user interface.

Obviously, Window is the interface of the fluent framework to connect the host operating system,

Let's take a look at some definitions of the Window class:

@Native("Window,DOMWindow")
class Window extends EventTarget  implements WindowEventHandlers,  WindowBase  GlobalEventHandlers,
        _WindowTimers, WindowBase64 {
          
  // DPI of the current device, that is, how many physical pixels are displayed in a logical pixel. The larger the number, the finer the display effect and fidelity.
  // DPI is the firmware attribute of the device screen. For example, the DPI of Nexus 6 screen is 3.5 
  double get devicePixelRatio => _devicePixelRatio;
  
  // The size of the drawing area of the fluent UI
  Size get physicalSize => _physicalSize;

  // The default language of the current system is Locale
  Locale get locale;
    
  // Current system font scaling.  
  double get textScaleFactor => _textScaleFactor;  
    
  // Callback when drawing area size changes
  VoidCallback get onMetricsChanged => _onMetricsChanged;  
  // Callback when Locale changes
  VoidCallback get onLocaleChanged => _onLocaleChanged;
  // System font scaling change callback
  VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
  // The callback before drawing is generally driven by the vertical synchronization signal VSync of the display and will be called when the screen is refreshed
  FrameCallback get onBeginFrame => _onBeginFrame;
  // Draw callback  
  VoidCallback get onDrawFrame => _onDrawFrame;
  // Click or pointer event callback
  PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
  // Scheduling Frame. After the method is executed, onBeginFrame and onDrawFrame will be called at the right time,
  // This method will directly call the window of fluent engine_ Scheduleframe method
  void scheduleFrame() native 'Window_scheduleFrame';
  // To update the rendering applied on GPU, this method will directly call the window of fluent engine_ Render method
  void render(Scene scene) native 'Window_render';

  // Send platform message
  void sendPlatformMessage(String name,
                           ByteData data,
                           PlatformMessageResponseCallback callback) ;
  // Platform channel message processing callback  
  PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
  
  ... //Other properties and callbacks
    
}        
Copy code

You can see that the Window contains some information about the current device and system and some callbacks from the shuttle engine.

Now let's look back at the various bindings mixed with WidgetsFlutterBinding. By viewing the source code of these bindings, we can find that these bindings basically listen to and process some events in Window objects, and then install these events into the Framework model for packaging, abstraction and distribution. You can see that WidgetsFlutterBinding is the glue that adheres the Flutter engine to the upper Framework.

  • GestureBinding: provides window The onpointerdatapacket callback is bound to the Fragment gesture subsystem and is the binding entry between the Framework event model and the underlying events.
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    window.onPointerDataPacket = _handlePointerDataPacket;
  }
}
  • ServiceBinidng: provides window Onplatformmessage callback. The user binds to the platform message channel, which mainly handles native and fluent communication.
mixin SchedulerBinding on BindingBase {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    if (!kReleaseMode) {
      addTimingsCallback((List<FrameTiming> timings) {
        timings.forEach(_profileFramePostEvent);
      });
    }
  }
 
  • SchedulerBinding: provides window Onbeginframe and window The ondrawframe callback listens to refresh events and binds the Framework drawing scheduling subsystem.
  • PaintingBinding: binding the drawing library, which is mainly used by users to process the picture cache
  • SemanticsBidning: the bridge between semantic layer and fluent engine, mainly the bottom support of auxiliary functions.
  • RendererBinding: provides window onMetricsChanged ´╝îwindow.onTextScaleFactorChanged and other callbacks. He is the bridge between rendering tree and fluent engine.
  • WidgetsBinding: provides window Onlocalechange, onBulidScheduled and other callbacks. It is the bridge between the fluent widget layer and engine.

widgetsFlutterBinding. Ensureinitialized() is responsible for initializing a global singleton of widgetsBinding, and then calling the attachRootwWidget method of WidgetBinding, which is responsible for adding the root Widget to RenderView. The code is as follows:

void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}
Copy code
void attachRootWidget(Widget rootWidget) {
  final bool isBootstrapFrame = renderViewElement == null;
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
  if (isBootstrapFrame) {
    SchedulerBinding.instance!.ensureVisualUpdate();
  }
}
Copy code

Note that there are two variables renderView and renderviveelement in the code. renderView is a Renderobject, which is the root of the rendering tree. Renderviveelement is the Element object corresponding to renderView.

It can be seen that this method mainly completes the whole association process from root widget to root RenderObject and then to root Element. Let's look at the source code implementation process of attachToRenderTree:

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
  if (element == null) {
    owner.lockState(() {
      element = createElement();
      assert(element != null);
      element!.assignOwner(owner);
    });
    owner.buildScope(element!, () {
      element!.mount(null, null);
    });
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element!;
}
Copy code

This method is responsible for creating the root element, RenderObjectToWidgetElement, and associating the element with the widget, that is, creating the element tree corresponding to the widget tree.

If the element has been created, set the associated widget in the root element as new. It can be seen that the element will be created only once and reused later. So what is buildouwner?, In fact, it is the management class of widget framework, which tracks which widgets need to be rebuilt.

After the component tree is built, it returns to the implementation of runApp. After the attachRootWidget is called, the scheduleWarmUpFrame() method of the widgetsflutterbinding instance will be called in the last line. The of this method is that in SchedulerBinding, it will draw immediately after being called. Before the drawing is finished, this method will lock the event distribution, That is to say, before the end of this drawing, Flutter will not respond to various events, which can ensure that new redrawing will not be triggered during the drawing process.

summary

Through the above analysis, we can know that widgetsflutterbinding is like a glue. It will listen to and process the events of window objects, and wrap and distribute these events according to the Framework model. Therefore, widgetsluterbinding is the glue that connects the fluent engine and the uploaded Framework.

  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
Copy code
  • ensureInitialized: it is responsible for initializing WidgetsFlutterBinding and listening to window events for packaging and distribution.
  • scheduleAttachRootWidget: in the follow-up of this method, the root element will be created and mount will be called to complete the creation of element and RenderObject trees
  • scheduleWarmUpFrame: start drawing the first frame

Render official line

Frame

A drawing process can be called a frame. We know that the fluent can achieve 60 fps, which means that it can redraw 60 times in one second. The larger the FPS, the smoother the interface will be.

It should be noted here that the frame in the shutter is not equal to the refresh frame of the screen, because the shutter UI framework does not trigger every screen refresh. This is because if the UI remains unchanged for a period of time, it is unnecessary to go through the rendering process again every time, Therefore, after the rendering of the first frame, fluent will actively request the frame to realize the rendering process. Only when the UI may change will it go through the rendering process again.

1. Fluent will register an onBeginFrame and an onDrawFrame callback on the window. In the onDrawFrame callback, drawFrame will eventually be called.

2. When we call window After the scheduleframe method, the Fletter engine will call onBeginFrame and onDrawFrame at the right time (it can be considered before the next refresh of the screen, depending on the implementation of the Fletter engine).

When calling window Before scheduleframe, onBeginFrame and onDrawFrame will be registered as follows:

void scheduleFrame() {
  if (_hasScheduledFrame || !framesEnabled)
    return;
  assert(() {
    if (debugPrintScheduleFrameStacks)
      debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
    return true;
  }());
  ensureFrameCallbacksRegistered();
  window.scheduleFrame();
  _hasScheduledFrame = true;
}

 void ensureFrameCallbacksRegistered() {
    window.onBeginFrame ??= _handleBeginFrame;
    window.onDrawFrame ??= _handleDrawFrame;
  }
Copy code

It can be seen that drawframe (this method is a registered callback) will be called only after the scheduleFrame is actively called.

Therefore, when we mention frame in fluent, unless otherwise specified, it corresponds to drawFrame(), not to the refresh of the screen.

Frame processing flow

When a new frame arrives, start calling schedulerbinding Handledrawframe is used to process the frame. The specific process is to execute four task queues: transientCallbacks, midFrameMicotasks, persistentCallbacks and postFrameCallbacks. When the four task queues are completed, the current frame ends.

To sum up, fluent divides the whole life cycle into five states, which are represented by SchedulerPhase:

enum SchedulerPhase {
  ///In idle state, there is no frame processing. This state indicates that the page has not changed and does not need to be re rendered
  ///If the page changes, you need to call scheduleFrame to request the frame.
  ///Note that the idle state simply means that there is no frame processing. Generally, micro tasks, timer callback or user callback events can be executed
  ///For example, after monitoring the tap event and the user clicks, our onTap callback is executed in onTap
  idle,

  ///Execute the temporary callback task. The temporary callback task can only be executed once and will be removed from the temporary task queue after execution.
  ///A typical example is that the animation callback will be executed at this stage
  transientCallbacks,

  ///When executing temporary tasks, new micro tasks may be generated, such as creating a Fluture when executing the first temporary task,
  ///And the Future has been resolve d before all tasks are completed
  ///In this case, the callback of Future will be executed in the [midFrameMicrotasks] phase
  midFrameMicrotasks,

  ///Perform some persistent tasks (tasks to be performed by each frame), such as rendering official lines (construction, layout, drawing)
  ///It is executed in this task queue
  persistentCallbacks,

  ///Before the end of the current frame, postFrameCallbacks will be executed. Usually, some cleaning work will be done and a new frame will be requested
  postFrameCallbacks,
}
Copy code

It should be noted that the rendering pipeline that needs to be highlighted next is executed in persistent callbacks.

Rendering pipeline

When our page needs to change, we need to call the scheduleFrame() method to request the frame, which will be registered_ handleBeginFrame and_ handleDrawFrame. When the frame arrives, it will be executed_ Handledrawframe, the code is as follows:

void _handleDrawFrame() {
  //Judge whether the current frame needs to be delayed. The reason for the delay here is that the current frame is a preheating frame	
  if (_rescheduleAfterWarmUpFrame) {
    _rescheduleAfterWarmUpFrame = false;
    //Add a callback that will be executed after the end of the current frame
    addPostFrameCallback((Duration timeStamp) {
      _hasScheduledFrame = false;
      //Re request frame.
      scheduleFrame();
    });
    return;
  }
  handleDrawFrame();
}
Copy code
void handleDrawFrame() {
  assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
  Timeline.finishSync(); // end the "Animate" phase
  try {
    // Toggles the current lifecycle state	
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
     // Callback to execute persistent tasks,  
    for (final FrameCallback callback in _persistentCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp!);

    // postFrame callback
    _schedulerPhase = SchedulerPhase.postFrameCallbacks;
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (final FrameCallback callback in localPostFrameCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp!);
  } finally {
     // Change state to idle state
    _schedulerPhase = SchedulerPhase.idle;
    Timeline.finishSync(); // end the Frame
     //....
    _currentFrameTimeStamp = null;
  }
}
Copy code

In the above code, the persistent task is traversed and called back, corresponding to_ persistentCallbacks. Through the analysis of the call stack, it is found that the callback is added to the call stack when initializing RendererBinding_ In persistentCallbacks:

mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
  @override
  void initInstances() {
    super.initInstances();
    //Add persistent task callback
    addPersistentFrameCallback(_handlePersistentFrameCallback);
    initMouseTracker();
    if (kIsWeb) {
      //Add postFrame task callback
      addPostFrameCallback(_handleWebFirstFrame);
    }
  }
  void addPersistentFrameCallback(FrameCallback callback) {
    _persistentCallbacks.add(callback);
  }
Copy code

So the final callback is_ handlePersistentFrameCallback

void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();
  _scheduleMouseTrackerUpdate();
}
Copy code

In the above code, the drawFrame method is invoked.

After the above analysis, we know that when the frame arrives, it will be called into the drawFrame. Because the drawFrame has an implementation method, it will first call the drawFrame() method of WidgetsBinding, as follows:

void drawFrame() {
  .....//Omission independent
  try {
    if (renderViewElement != null)
      buildOwner!.buildScope(renderViewElement!); // 1. Rebuild the widget tree
    super.drawFrame();
    buildOwner!.finalizeTree();
  } 
}
Copy code

The final call is as follows:

void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout(); // 2. Update layout
  pipelineOwner.flushCompositingBits();//3. Update "layer composition" information
  pipelineOwner.flushPaint(); // 4. Redraw
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // 5. On the screen, the drawn bit data will be sent to GPU
	...../////
  }
}
Copy code

The above code can do five main things:

1. Rebuild the widget tree (buildScope())

2. Update the layout (flushLayout())

3. Update "layer composition" information (flushCompositingBits())

4. Redraw (flushpaint)

5. On screen: display the drawn product on the screen

The above five parts are called rendering pipeline, which is translated into "rendering pipeline" or "rendering pipeline" in Chinese, and these five steps are the top priority. Let's take the update process of setState as an example. First, we have a deep impression of the whole update process.

setState execution flow

void setState(VoidCallback fn) {
  assert(fn != null);
  //Execute callback. The return value cannot be future  
  final Object? result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      throw ...//
    }
  }());
  _element!.markNeedsBuild();
}
Copy code
void markNeedsBuild() {
  ....//
  //Marking this element requires reconstruction
  _dirty = true;
  owner!.scheduleBuildFor(this);
}
Copy code
void scheduleBuildFor(Element element) {
 //Note 1   
 if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
     _scheduledFlushDirtyElements = true;
     onBuildScheduled!();
  }    
  //Note 2
  _dirtyElements.add(element);
  element._inDirtyList = true;
}
Copy code

After calling setState:

1. First call the markNeedsBuild method and mark the dirty of element as true, indicating that reconstruction is needed

2. Then call scheduleBuildFor to add the current element to the_ In dirtyElements list (Note 2)

Let's focus on the code of note 1,

First judge_ If scheduledFlushDirtyElements is false, the initial value of this field is false by default. Then judge that onBuildScheduled is not null. In fact, onBuildScheduled has been created when WidgetBinding is initialized, so it will not be null.

When the condition is satisfied, the onBuildScheduled callback will be executed directly. Let's follow up:

mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    ...///  
    buildOwner!.onBuildScheduled = _handleBuildScheduled
  }
Copy code
void _handleBuildScheduled() {
  ...///
  ensureVisualUpdate();
}
Copy code

According to the above code, we can know that onBuildScheduled is indeed initialized in the initialization method of WidgetsBinding. And in his implementation, he called the ensureVisualUpdate method. Let's continue to follow up:

void ensureVisualUpdate() {
  switch (schedulerPhase) {
    case SchedulerPhase.idle:
    case SchedulerPhase.postFrameCallbacks:
      scheduleFrame();
      return;
    case SchedulerPhase.transientCallbacks:
    case SchedulerPhase.midFrameMicrotasks:
    case SchedulerPhase.persistentCallbacks:
      return;
  }
}
Copy code

In the above code, the state of schedulerPhase is judged. If it is in the state of idle and postFrameCallbacks, scheduleFrame will be called.

The meaning of each of the above states has been mentioned in the article, so I won't repeat it here. It is worth mentioning that each time the frame process is completed, the state is changed to idle in the finally code block. This also shows that if you frequently use setState, if the last rendering process is not completed, a new rendering will not be initiated.

Then continue to look at the scheduleFrame:

void scheduleFrame() {
  //Determine whether the process has started
  if (_hasScheduledFrame || !framesEnabled)
    return;
  // Note 1
  ensureFrameCallbacksRegistered();
  // Note 2
  window.scheduleFrame();
  _hasScheduledFrame = true;
}
Copy code

Note 1: register onBeginFrame and onDrawFrame. The fields of these two function types have been mentioned in "rendering pipeline" above.

Note 2: the shuttle framework sends a request to the shuttle engine, and then the shuttle engine will call onBeginFrame and onDrawFrame at the right time. This time can be considered as before the next refresh of the screen, which depends on the implementation of the fluent engine.

At this point, the core of setState is to trigger a request, which will call back onBeginFrame at the next screen refresh, and the onDrawFrame method will not be called until the execution is completed.

void handleBeginFrame(Duration? rawTimeStamp) {
  ...///
  assert(schedulerPhase == SchedulerPhase.idle);
  _hasScheduledFrame = false;
  try {
    Timeline.startSync('Animate', arguments: timelineArgumentsIndicatingLandmarkEvent);
    //Change the life cycle to transientCallbacks, indicating that some callbacks of temporary tasks are being executed
    _schedulerPhase = SchedulerPhase.transientCallbacks;
    final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
    _transientCallbacks = <int, _FrameCallbackEntry>{};
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
      if (!_removedIds.contains(id))
        _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
    });
    _removedIds.clear();
  } finally {
    _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
  }
}
Copy code

The above code is mainly executed_ Callback method of transientCallbacks. After execution, change the life cycle to midFrameMicrotasks.

The next step is to execute the handlerDrawFrame method. This method has been analyzed above, and it is known that it will eventually go to the drawFrame method.

# WidgetsBindign.drawFrame()
void drawFrame() { 
.....//Omission independent
  try {
    if (renderViewElement != null)
      buildOwner!.buildScope(renderViewElement!); // 1. Rebuild the widget tree
    super.drawFrame();
    buildOwner!.finalizeTree();
  } 
}
# RendererBinding.drawFrame()
void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout(); // 2. Update layout
  pipelineOwner.flushCompositingBits();//3. Update "layer composition" information
  pipelineOwner.flushPaint(); // 4. Redraw
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // 5. On the screen, the drawn bit data will be sent to GPU
	...../////
  }
}
Copy code

The above is the general process of setState calling. The actual process will be more complex. For example, it is not allowed to call setState again in this process, and the scheduling of animation and how to update and redraw the layout will be involved in the frame. Through the above analysis, we need to have a deep impression of the whole process.

As for the drawing process in the drawFrame above, we will introduce it in the next article.

reference material