When FlutterEngine was started in the previous article, we analyzed the initialization process of FlutterEngine from the Andorid terminal to the main() method of the FlutterUI layer. Before the main() method is called, we have stored the window.dart file during FlutterEngine initialization. After running the main() function, we add each frame generated by the FlutterUI layer to the FlutterEngine. Next, we will further analyze the FlutterUI layer code of the FlutterEngine shelf and the initialization logic of the FlutterUI layer

The profile

Core classes:

RenderObject: RenderBox implementation class

Element: Manages the logical relationship between widgets in the tree

Wideget: Configure the data on each node

BuildContext: The user interface to look up in the provided Element tree

Window: The interface provided by FlutterEngine for FlutterUI communication

RenderView: The first RenderObject of the FlutterUI framework, associated with the Window Scene

Layer: A logical representation of each rendered object

Scene: The final FlutterUI layer generates Scene for SkyEngine through a series of layout, composition and summary

RenderObjectToWidgetAdapter: FlutterUI first Widget object

BuildOwner: Logs nodes added and removed from the Element tree, and logs which elements need to be recalculated

PipelineOwner: Logs the relationships and changes requested by the RenderObject in the entire tree, and finally calls PipelineOwner to process each frame logically through the tree

Engine rendering principle

Before analyzing the FlutterUI framework, how does the Flutter engine draw a frame onto the screen in the least number of steps

Elephant in freezer (Maximum particle decomposition Action)

1. Calling scheduleFrame() when a Flutter starts triggers the uI.window.onBeginFrame () callback

2. SceneBuilder generates the scene

3. Return the data to the render engine :ui.window.render(scene);

4. Gestures callback event: UI window. OnPointerDataPacket = handlePointerDataPacket; Trigger a continuous call to the method in step 3 through a gesture event

5. Refresh a frame: uI.window.scheduleFrame ();

Through the above three steps, you can achieve a frame by frame drawing on the screen, the main content is how to manage the Scene object data change processing logic, constantly update the data back to the rendering engine for calculation

The official Demo

Official Demo source

ui.Picture paint(ui.Rect paintBounds) {
  final ui.PictureRecorder recorder = ui.PictureRecorder();
  final ui.Canvas canvas = ui.Canvas(recorder, paintBounds);
  final ui.Size size = paintBounds.size;
  canvas.drawCircle(
    size.center(ui.Offset.zero),
    size.shortestSide * 0.45, ui.Paint().. color = color, );return recorder.endRecording();
}

ui.Scene composite(ui.Picture picture, ui.Rect paintBounds) {
  final double devicePixelRatio = ui.window.devicePixelRatio;
  final Float64List deviceTransform = Float64List(16)
    ..[0] = devicePixelRatio .. [5] = devicePixelRatio .. [10] = 1.0
    ..[15] = 1.0;
  finalui.SceneBuilder sceneBuilder = ui.SceneBuilder() .. pushTransform(deviceTransform) .. addPicture(ui.Offset.zero, picture) .. pop();return sceneBuilder.build();
}

void beginFrame(Duration timeStamp) {
  final ui.Rect paintBounds = ui.Offset.zero & (ui.window.physicalSize / ui.window.devicePixelRatio);
  final ui.Picture picture = paint(paintBounds);
  final ui.Scene scene = composite(picture, paintBounds);
  ui.window.render(scene);
}

void handlePointerDataPacket(ui.PointerDataPacket packet) {
  for (ui.PointerData datum in packet.data) {
    if (datum.change == ui.PointerChange.down) {
      color = const ui.Color(0xFF0000FF);
      ui.window.scheduleFrame();
    } else if (datum.change == ui.PointerChange.up) {
      color = const ui.Color(0xFF00FF00);
      ui.window.scheduleFrame(); }}}void main() {
  color = const ui.Color(0xFF00FF00);
  // The engine calls onBeginFrame whenever it wants us to produce a frame.
  ui.window.onBeginFrame = beginFrame;
  // The engine calls onPointerDataPacket whenever it had updated information
  // about the pointers directed at our app.
  ui.window.onPointerDataPacket = handlePointerDataPacket;
  // Here we kick off the whole process by asking the engine to schedule a new
  // frame. The engine will eventually call onBeginFrame when it is time for us
  // to actually produce the frame.
  ui.window.scheduleFrame();
}

Copy the code

According to the above logic, the ultimate purpose of The FlutterUI layer is to provide a frame content to transmit to an image rendering engine, which continuously drives it to modify the Widget related attributes, thus changing the data related to the FlutterRenderObject and providing it to the FlutterEngine for processing. Finally render to the View provided by the platform

Flutter architecture

Core problem points

From the above analysis, we know that the main concern of the Flutter engine is to draw the UI layer:

1. Render object output

2. How do render objects change

3. How to manage the generation of render objects

4. How do I manage object changes in the tree

tree.png

System layer initialization process

The Widget initialization process

The UI initialization process of the Flutter layer is to associate the application Widget with the system’s rendering engine by calling runApp in the entry file

voidrunApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code

In the previous article, we showed how Window events are decomposed and initialized into different subclasses of the “BaseBind” object, and continued our analysis of how user-level widgets relate to the system-wide Window framework.

AttachRootWidget completes the binding of the RenderObject to the Element, the first RenderObject associated with the Window object, and calls BuilderOwer to calculate the areas displayed on the screen and widgets that need to be redrawn. BuilderOwer manages Element objects from roots to leaves, keeping a record of what needs to be changed (if a tree has 10 nodes, then only after the first drawing is done, if several nodes in the tree have changed, during the second building of the rendering object, start from the Root node, There is a need to walk through the tree to find which node has changed. Use BuilderOwer in the Root render object to save whether each node has changed or not, for the next build render object :1. Managing widgets framework, 2. Managing Elements that are not Inactive,3. Manage the entire Widget tree reassemble command, build/layout/paint pipeline), the distribution of BuilderOwer to the whole tree, through RootRenderObjectElement. AssignOwner

ScheduleWarmUpFrame After all the data is ready, a frame of data needs to be passed to the system FlutterEngine to continue rendering on the development screen. Pipeline is a management logic that processes the calculated data and submits it to FluuterEngine for rendering

The above logic mainly does two operations:

1. Generate the associated object of static data :(just save the logical data)

RenderView: the data abstraction of the Surface object corresponding to the system layer rendering framework, the first rendering object of the FlutterUI layer. The Widget holds the type of data structure that controls it. The Element object is an abstract representation of the Widget that tells BuilderOwer how to configure the Widget. Element of the life cycle _ElementLifecycle {initial, active, inactive, defunct,}, call ` attachRenderObject ` rendering object mounted to the render tree 3.RenderObject: a coordinate system in which elements are logically arranged in Cartesian or polar coordinates by subclass 'RenderBox'. Provide the drawing Layer Layer 4. BuildContext: in Widget provides a user access ` RenderObject ` interface class 5. RenderObjectToWidgetAdapter: 'RootRenderObjectElement' and 'RenderView' are managed together. The root 'RootRenderObjectElement' holds the object 'RenderView'. Then the Element and RenderObject are together and can update data synchronously. The Widget cannot be modified to save data in 'RenderObject'. When the Widget changes, specify the UI update, save the data in 'RenderObject', and rebuild the 'RenderObject' object. Unless the parent holding the data is not removed from the build tree. 7.RenderObject->Constraints describes the data that the sub-wiget can read. Cartesian and polar coordinates are not describedCopy the code

2. Dynamic data

BuildOwner: start with 'RenderView' to calculate which part of the UI is displayed and which part needs to be updated. Save the related 'List<Element>' object 2.[PipelineOwner](.. / flutterdev/FlutterWidgetinit / # pipeline) : each Element ` ` will have a ` RenderObject `, This is used to process the rendering engine's logical data in the FlutterUI layer. The previous step added the data that needed to be updated to 'List<RenderObject>' 3. The 'compositeFrame' method of 'RenderView' is called to pass scene data to the system rendering engineCopy the code

RenderObject and Element relationships

By calling the runApp attachRootWidget connecting Element and RenderObject RenderObjectToWidgetAdapter is a subclass of the Widget, to achieve two main methods:

1.`createElement`
2.`createRenderObject`
Copy the code

The main function of createElement is to manage widgets. Element is an instance object of a Widget, and Elment has different subtypes. Element acts as a single node in the tree and manages renderObjects and widgets. The Widget provides the data configuration of the first node, Element, which also manages the logical relationship of each node in the tree. Multiple widgets are connected in series by Element to form a tree. Element functions as a drawing, which is equivalent to a map, and can actually provide a road for each car. That’s the main function of RenderObject

The main function of createRenderObject is to generate RenderObject. The main function is to complete the construction of coordinate system and provide configuration parameters for Element. The implementation class is RenderBox. Finding changes in the RenderObject can be done from the Pipeline instead of traversing the entire tree

Each Element corresponds to a RenderObject. RenderObject is used as an Element member variable. Multiple elements are associated to form a tree. RenderObject on each Element node is synthesized to provide each frame on the FlutterEngine. Element is used to implement logical functions. RenderObject implements real data rendering functions

RenderObjectToWidgetAdapterWhere do the arguments in the constructor of?

renderView: WidgetsBinding inherits RendererBinding, The renderView is initialized in the initInstances method of the RendererBinding. The RenderBox member variable is the Root of the entire drawing Top-level RootRenderObjectElement, using BuildOwner only for top-level objects

RenderObjectToWidgetAdapter class as the top-level UI Flutter system, Act as a joint point between the user UI and the Flutter framework layer The RenderView becomes a member variable of RootRenderObjectElement. The render object is associated with the Element object. The UI can be updated synchronously

AttachRootWidget does three things:

RenderObjectToWidgetAdapter: association renderView and rootWidget

Associated PipelineOwner

Associated BuildOwner

void attachRootWidget(Widget rootWidget) {
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner, renderViewElement);
}
Copy the code

RenderView object

RenderView objects are initialized RendererBinding invokes the initRenderView object, renderView integration RenderObjectWithChildMixin RenderObject implementation

RenderBox: is the real RenderObject RenderObject: 1. We store the Constraints relationship between the actual data and the Child, as well as the interface to the RenderBox

2. Save ParentData associated with Child

/// Creates a [RenderView] object to be the root of the
/// [RenderObject] rendering tree, and initializes it so that it
/// will be rendered when the engine is next ready to display a
/// frame.
///
/// Called automatically when the binding is created.
void initRenderView() {
  assert(renderView == null);
  renderView = RenderView(configuration: createViewConfiguration(), window: window);
  renderView.scheduleInitialFrame();
}
Copy the code

ViewConfiguration

Configures the size of the FlutterUI framework’s first render object to display on screen

1. Zoom ratio of device pixels

2. Time physical pixels of the device

ViewConfiguration createViewConfiguration() {
  final double devicePixelRatio = window.devicePixelRatio;
  return ViewConfiguration(
    size: window.physicalSize / devicePixelRatio,
    devicePixelRatio: devicePixelRatio,
  );
}
Copy the code

Window

When the BindingBase object is initialized, the UI. window object is the object that should communicate with the Flutter

ui.Window get window => ui.window;

scheduleInitialFrame

After the RenderView is initialized, the initialization marks the current state of the rendered object

1. Add the current object to the Pipline and mark it as _nodesNeedingLayout

2. Initialize the Layer object, and all RenderObject objects will eventually be merged into the current object and passed to window.render

3. Notify Pipline of visual updates

void scheduleInitialFrame() {
  assert(owner ! =null);
  assert(_rootTransform == null);
  scheduleInitialLayout();
  scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
  assert(_rootTransform ! =null);
  owner.requestVisualUpdate();
}
Copy the code

Layer

The Layer is where all the RenderObject objects are composited together and passed to window.render

1. Initialize a Layer object

2. Bind RootLayer to the current render object rootLayer.attach(this); Each renderObject holds a reference to the RootLayer object

Layer _updateMatricesAndCreateNewRootLayer() {
  _rootTransform = configuration.toMatrix();
  final ContainerLayer rootLayer = TransformLayer(transform: _rootTransform);
  rootLayer.attach(this);
  assert(_rootTransform ! =null);
  return rootLayer;
}
Copy the code

The render object has been initialized and associated to the PipelineOwner object using the initialization process above, which is described in more detail later (owner._nodesneedinglayout.add (this);). The RenderView initializer sets the size of the current FlutterUI display on the screen, associated with objects in the FlutterEngine layer, and passes a frame of data to the local methods of the Window object

summary

The RenderView object is initialized:

1. Call initRenderView() to initialize RootRenderView when RendererBinding object is initialized

2. Obtain the ViewConfiguration object of the device to configure the size of FlutterUI displayed on the window

3. Get the UI. Window object reference. FlutterUI layer frames can be synthesized into the window.render method

4. Create a Layer object as RootLayer and pass it to the RenderObject on each node by calling the attch method

User layer Widget initialization process

Initialize theWidget

RenderView and PipelineOwner (PipelineOwner), which perform measurement, layout, and rendering of Widget changes, have initialized various BaseBind subclasses and decomposed Window events

RenderObjectToWidgetAdapter. AttachToRenderTree to create this Widget to fill of Element, and the actual set results [renderobject] to [container] children. If element is empty, this function creates a new element. Otherwise, the given element will have a scheduled update to switch to this widget. Used by [runApp] to boot the application. Only one [buildScope] can be active at a time.

The attach object is called, the renderViewElement object is null, the tree is recursively traversed from the root node, all the widgets in the tree are filled, and the dependencies of the parent and sibling nodes are stored through the Element object. Every node configuration information is provided through the Widget interface to operate the whole tree interface, rendering RenderObject RenderObjectToWidgetElement

Initialize the Element

Initialize the Element object after binding the render object to the Element

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
  if (element == null) {// the top-level native calls createment() manually; RenderObjectToWidgetElement
    owner.lockState(() {
      element = createElement();
      assert(element ! =null);
      /// Only the root node calls this method. The other nodes do not have empty elements
      element.assignOwner(owner);
    });
    owner.buildScope(element, () {
      element.mount(null.null);  /// Start traversing the tree to process each Widget
    });
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element;
}

Copy the code

CreateElement method method to create rootElement object, RenderObjectToWidgetElement, there are four members of the Element object variables:

RenderObject: The renderObject of the current Widget object

2._newwidget: Provides developers with an interface to control RenderObject

3._child: If the current Element object has child nodes, use the current variable to hold the reference to the current child node

Element.mount, create RenderObject

When createElement() is called to create an Element object, the Element object is isolated and the parent Element is not responsible for mounting it. The child Element mount method performs two operations:

1. Call super.mount() to tell the parent Element to add itself to the entire tree and create a render object

2. Update the Element object of the child node and repeat the process

RenderObjectElement.mount()

At the time of creating RenderObjectToWidgetElement objects, in the constructor calls RenderObjectToWidgetElement (RenderObjectToWidgetAdapter widget) : super(widget); , passes the widget object of the current node, initializes it when calling mount, creates the render object by calling createRenderObject, and attachRenderObject(newSlot); The RenderObject is created and added to the render tree. This completes the initialization of the current node. If the current node has child nodes, the current node’s children are initialized.

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  assert(() { _debugUpdateRenderObjectOwner(); return true; } ());assert(_slot == newSlot);
  attachRenderObject(newSlot);
  _dirty = false;
}

Copy the code

updateChild

Starting from the root node, you create child Element objects, mount them to the Element tree, and initialize their child nodes with updateChild. The condition for updating child nodes is whether to update the current child node, create a new one, or reuse the current node

Core algorithm for whether to create a node:

/// The following table summarizes the above: /// /// | | **newWidget == null** | **newWidget ! = null** | /// | :-----------------: | :--------------------- | :---------------------- | /// | **child == null** | Returns null. | Returns new [Element]. | /// | **child ! = null** | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |Copy the code
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  assert(() {
    if(newWidget ! =null && newWidget.key is GlobalKey) {
      final GlobalKey key = newWidget.key;
      key._debugReserveFor(this);
    }
    return true; } ());if (newWidget == null) {
    if(child ! =null)
      deactivateChild(child);
    return null;
  }
  if(child ! =null) {
    if (child.widget == newWidget) {
      if(child.slot ! = newSlot) updateSlotForChild(child, newSlot);return child;
    }
    if (Widget.canUpdate(child.widget, newWidget)) {
      if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);assert(child.widget == newWidget);
      assert(() {
        child.owner._debugElementWasRebuilt(child);
        return true; } ());return child;
    }
    deactivateChild(child);
    assert(child._parent == null);
  }
  return inflateWidget(newWidget, newSlot);
}
Copy the code

InflateWidget is where an Element is actually created

Creates an element for the given widget and adds it as an element in the given slot. This method is usually called by [updatechild], but can be called directly through subclass elements that require more fine-grained control over creation. If the given widget has a global key and an element already exists with a widget with that global key, this function will reuse the element possibly grafted or reactivated from another location in the tree (it from the list of Inactive Elements) rather than create a new element. The element returned by this function has already been loaded and will be in the activity life cycle state. Note: The above is to continue the analysis of the logic used in the UI architecture, mainly is the whole UI framework of data representation and association to split and combine, can provide the UI changes are the data tracking and recording (static data preservation logic)

final Element newChild = newWidget.createElement(); Walk through the tree, generate an instance object Element for each Widget, and call the mount method to add Elment to the tree

@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
  assert(newWidget ! =null);
  final Key key = newWidget.key;
  if (key is GlobalKey) {
    final Element newChild = _retakeInactiveElement(key, newWidget);
    if(newChild ! =null) {
      assert(newChild._parent == null);
      assert(() { _debugCheckForCycles(newChild); return true; } ()); newChild._activateWithParent(this, newSlot);
      final Element updatedChild = updateChild(newChild, newWidget, newSlot);
      assert(newChild == updatedChild);
      returnupdatedChild; }}final Element newChild = newWidget.createElement();
  assert(() { _debugCheckForCycles(newChild); return true; } ()); newChild.mount(this, newSlot);
  assert(newChild._debugLifecycleState == _ElementLifecycle.active);
  return newChild;
}

Copy the code

BuildOwner:

The Manager class for the Widgets framework. This class tracks the widgets that need to be rebuilt and handles other tasks applied to the widget tree as a whole, such as managing the inactive element listing tree and hot reloading when the “reassemble” command is triggered for debugging if necessary.

  1. Usually owned by [widgetsBinding] and with build/ Layout /paint pipes. Additional build owners can be generated to manage the off-screen widget tree. To assign a build owner to a tree

  2. Please use the root element of [rootrenderobjectelement assignowner] method.

BuildOwner: The BuildOwner object is used to render objects in the Element tree that need to be changed to find out which elements are contaminated and need to be rearranged and drawn by looking at the BuildOwner object’s member variables:

1. Record the information added to or removed from the entire tree

final _InactiveElements _inactiveElements = _InactiveElements();

2. Log and add Element objects to the tree. Those Widget objects have changed

final List _dirtyElements = [];

Reassemble: own call on hotreload

Causes the root to be completely rebuilt in the entire subtree of the given element. When the application code has changed and is being hot reloaded, the widget tree gets any changed implementations. This is expensive and should not be invoked except during development. DrawFrame is called in the initInstances method of RendererBinding

BuildScope:

Look for elements in the area displayed on the screen and determine which elements are to be processed. The canvas is an area with no boundaries, so you need to confirm which areas need to be processed before drawing

void drawFrame() {
  .....

  try {
    if(renderViewElement ! =null)
    /// Start calculating the range to display a frame on the screen
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
    buildOwner.finalizeTree();
  } finally{... }... }Copy the code

Summary of the Widget initialization process

From the above we know the entire Widget initialization process:

1. Call the createElement method of the Widget object to create an Element object for the current node

2. Call the mount method on Element

1. Add itself to the entire Element object by calling the mount method of the parent class. In the mount of the parent method call widget. CreateRenderObject create 3 RenderOb object of the current node. Call the updateChild method to iterate over the Widget objects of all the child nodes and add them to the Element tree and RenderObject treeCopy the code

3. The WidgetsBinding member variable BuildOwner is used to record the addition and removal of the entire Element tree.

So far, the entire widget tree has been initialized, and the configuration information of the entire tree has been initialized. This part of the widget tree is static data from which the data of each frame seen on the screen is collected and calculated.

Draw the first frame

When main() is called, runApp() is initialized, the entire FlutterUI framework is loaded, and the current RenderObject is rendered into the FlutterEngine

  1. AttachRootWidget: Create the Element tree and RenderObject tree at the same timeBuildOwnerTo track changes to the entire Element object,PiplineRecord changes to the RenderObject

RenderObject initialization is complete, and scheduleWarmUpFrame is displayed on the screen

voidrunApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code

Trigger the first frame to execute as soon as possible, rather than waiting for the engine to request the frame in response to the system “vsync” signal. This is used during application startup so that the first frame (which can be expensive) gets some extra milliseconds to run. Lock the event schedule until the scheduled frame completes. If a frame is already scheduled with [scheduleFrame], or [scheduleForcedFrame], this call may delay the frame. This call is ignored if any scheduled frames have already started, or if another one has already called scheduleWarmupFrame. Prefer [scheduleFrame] To update the display during normal operation. Calling scheduleFrame triggers the window object to process the first frame window.scheduleFrame(); The Window. OnBeginFrame is triggered and execution begins. The SchedulerBinding handles the frame callback event for the Window object

void scheduleWarmUpFrame() {
  if(_warmUpFrame || schedulerPhase ! = SchedulerPhase.idle)return;
  _warmUpFrame = true;
  Timeline.startSync('Warm-up frame');
  final bool hadScheduledFrame = _hasScheduledFrame;
  Timer.run(() {
    assert(_warmUpFrame);
    handleBeginFrame(null);
  });
  Timer.run(() {
    assert(_warmUpFrame);
    handleDrawFrame();
    resetEpoch();
    _warmUpFrame = false;
    if (hadScheduledFrame)
      scheduleFrame();
  });
  lockEvents(() async {
    await endOfFrame;
    Timeline.finishSync();
  });
}
Copy the code

handleBeginFrame

When the RendererBinding initializes initInstances, AddPersistentFrameCallback _handlePersistentFrameCallback add processing a frame callback functions, in the previous step calls scheduleFrame triggered () the window. The onDrawFrame callback function

The handleBeginFrame method preprocesses a frame into the FlutterEngine,

void handleBeginFrame(Duration rawTimeStamp) {
  Timeline.startSync('Frame', arguments: timelineWhitelistArguments); _firstRawTimeStampInEpoch ?? = rawTimeStamp; _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);if(rawTimeStamp ! =null) _lastRawTimeStamp = rawTimeStamp; .assert(schedulerPhase == SchedulerPhase.idle);
  _hasScheduledFrame = false;
  try {
    // TRANSIENT FRAME CALLBACKS
    Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
    _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 the code

The handleDrawFrame first calls the changes in the FlutterUI layer, and the RendererBinding initialization adds callback methods to the persistent callback collection

void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();
}

Copy the code

drawFrame()

The drawFrame() method is called to draw the first frame. In the above analysis process, the PipLine is analyzed to save the change information of the RenderObject. The next step is to call the PipLine to search the RenderObject. Layout, composition, draw, etc., and finally a frame is ready, and then FlutterEngine is notified that the data frame is ready to render…

/// Called by the engine to produce a new frame.
///
/// This method is called immediately after [handleBeginFrame]. It calls all
/// the callbacks registered by [addPersistentFrameCallback], which typically
/// drive the rendering pipeline, and then calls the callbacks registered by
/// [addPostFrameCallback].
///
/// See [handleBeginFrame] for a discussion about debugging hooks that may be
/// useful when working with frame callbacks.
void handleDrawFrame() {
  assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
  Timeline.finishSync(); // end the "Animate" phase
  try {
    // PERSISTENT FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
    for (FrameCallback callback in _persistentCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);

    // POST-FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.postFrameCallbacks;
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (FrameCallback callback in localPostFrameCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);
  } finally {
    _schedulerPhase = SchedulerPhase.idle;
    Timeline.finishSync(); // end the Frame
    assert(() {
      if (debugPrintEndFrameBanner)
        debugPrint('▀' * _debugBanner.length);
      _debugBanner = null;
      return true; } ()); _currentFrameTimeStamp =null; }}Copy the code

Pipeline

PipelineOwner provides an interface that drives PipelineOwner and stores the state of rendered objects that have requested access at each stage of the pipeline. To refresh PipelineOwner, call the following command functions in sequence:

1.[flushLayout] updates anything that needs to be computed for its layout. In this stage, the size and position of each rendered object is calculated. Render objects may stain their painting or compositing state at this stage.

2.[flushCompositingbits] Updates any dirty render object composition bits. In this stage, each render object will learn that any of its children need to be synthesized. This information is used to select how to implement visual effects during the drawing phase, such as clipping. If the render object has composited child objects, the clip needs to be created using [layer] so that the clip is applied to the Composited Child (which will be drawn to its own [Layer]).

3.[flushpaint] accesses any render object that needs to be drawn. During this phase, render objects have the opportunity to record draw commands to [PictureLayer] and construct other synthetic [Layer]s.

4. Finally, if semantics is enabled, [FlushsSemantics] compiles the semantics of the rendering object. This semantic information is provided by assistive techniques that improve the accessibility of render trees. [renderBinding] keeps the pipe owner of the rendered object visible on the screen. You can create additional pipe owners to manage the off-screen object, which can be independent of the render object on the screen.

When does pipeline start calling?

voidrunApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code

This method is called by handleDrawFrame, which itself is called and executed automatically by the engine when the frame is laid out and drawn. Each frame consists of the following phases:

1. Animation stage: register handleBeginFrame method windod. onBeginFrame, call all temporary frame callback according to the registration order to scheduleFrameCallback registration. This contains all of the instance objects being driven, which means all of the animation object points that are active.

2.Microtasks: Any run scheduled by a temporary frame callback after handleBeginFrame returns. This usually involves the ticker and AnimationController’s futures callbacks to complete this frame. So after handleBeginFrame, handleDrawFrame, it’s registered to call window.onDrawFrame, and it calls all the persistent frame callbacks, and the most notable one is this method, drawFrame, which is called

3. The layout phase: system all dirty renderobject is placed out (see renderobject. Performlayout). See renderobject. Markneedslayout about the layout of the object will be marked as dirty details.

4. Composition bit stage: Any dirty files on which the composition bit renderObject object has been updated. RenderObject markneedscompositingbitsupdate.

5. Paint phase: All dirty renderObjects in the system are redrawn (see renderObject.paint). This will generate the Layer tree. Renderbject. Markneedspaint know detailed information about the object tag paint is dirty.

6. Composition stage: Layer tree turns into a scene and sends it to GPU. 7. Semantic stage: in the system all dirty renderobject have their semantics has been updated (see renderobject. Semanticsannotator). This generates a SemanticsNode tree. Lake renderbject. Markneedssemanticsupdate understanding on markup semantic dirty object. For more information on steps 3-7, see PipelineOwner.

8. Finalization phase: After DrawFrame returns, HandleDrawFrame then calls the postframe callback (registered with addPostFrameCallback). Some bindings (such as WidgetsBinding) add an extra step list for this (for example, see WidgetsBinding. Drawframe).

@protected
void drawFrame() {
  assert(renderView ! =null);
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
Copy the code

flushLayout

PipelineOwner uses the _nodesNeedingLayout list to locate renderObjects that need to be stored in the PipelineOwner layout list. The PipelineOwner calls the _layoutWithoutResize method to change the layout size. Also add the RenderObject to the _nodesNeedingPaint collection for the next phase, changing _needsPaint to true

void flushLayout() {
....
  try {
    // TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselvesThe first time you create a RenderObject, you need a Layoutwhile (_nodesNeedingLayout.isNotEmpty) {
      final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
      _nodesNeedingLayout = <RenderObject>[];
      for (RenderObject node indirtyNodes.. sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {if (node._needsLayout && node.owner == this) node._layoutWithoutResize(); }}}finally{... }}Copy the code

In the above analysis, we already know that the first rendering object of FlutterUI is the RenderView, which is the first rendering object to be called when the layout starts. The RenderView object inherits the RenderView object. RenderObject _layoutWithoutResize calls RenderView’s performLayout method to start drawing the entire screen from RootRenderObject.

void _layoutWithoutResize() {
  assert(_relayoutBoundary == this);
  RenderObject debugPreviousActiveLayout;
  assert(! _debugMutationsLocked);assert(! _doingThisLayoutWithCallback);assert(_debugCanParentUseSize ! =null);
  assert(() {
    _debugMutationsLocked = true;
    _debugDoingThisLayout = true;
    debugPreviousActiveLayout = _debugActiveLayout;
    _debugActiveLayout = this;
    if (debugPrintLayouts)
      debugPrint('Laying out (without resize) $this');
    return true; } ());try {
    performLayout();
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    _debugReportException('performLayout', e, stack);
  }
  assert(() {
    _debugActiveLayout = debugPreviousActiveLayout;
    _debugDoingThisLayout = false;
    _debugMutationsLocked = false;
    return true; } ()); _needsLayout =false;
  markNeedsPaint();
}

Copy the code

How to calculate the placement of render objects and customize the RenderView control

Calculates the layout of this render object. This method is the main entry point that the parent controls ask the children to perform to update their layout information. The parent object passes the constraint object that tells the children which layouts are allowed. The child control must comply with the given constraints. The parent of “parentUseSSize” must pass true if the parent reads information computed during the child’s layout. In this case, the parent will: whenever the child is marked as needing a layout, it will be marked as needing a layout because the parent’s layout information depends on the child’s layout information. If the parent uses the default (false) “parentUseSSize”, a child can change its layout information (based on a given constraint) without notifying the parent. Subclasses should not override [Layout] directly. Instead, they should rewrite [PerformResize] and/or [performLayout]. Delegates the actual work to [performResize] and [performLayout]. The parent [Performance Layout] method should call all of its unconditional children. This is the responsibility of the [layout] method implemented here.) If the child does not need to do anything, it returns early to update its layout information.

FlushLayout call node. _layoutWithoutResize (); Calculate the position of the render object. PerformLayout in RenderView performs layout processing

@override
  void performLayout() {
    assert(_rootTransform ! =null);
    _size = configuration.size;
    assert(_size.isFinite);

    if(child ! =null)
      child.layout(BoxConstraints.tight(_size));
  }
Copy the code

flushCompositingBits

If multiple controls overlap and need to be merged, how do I call markNeedsPaint to prepare the following summary


void flushCompositingBits() {
  if(! kReleaseMode) { Timeline.startSync('Compositing bits');
  }
  _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
  for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
    if (node._needsCompositingBitsUpdate && node.owner == this)
      node._updateCompositingBits();
  }
  _nodesNeedingCompositingBitsUpdate.clear();
  if (!kReleaseMode) {
    Timeline.finishSync();
  }
}
Copy the code

Marks the render object to compose while iterating through Layout. Call render their own parameters to determine whether synthesis, in the following the logic processing is a process optimization, to display the Widget on the screen, have a plenty of will overlap and cover, under the condition of the cover, if be completely cover off, there is no need to synthesis the RenderObject objects, mark the current render object is need to draw.

void _updateCompositingBits() {
  if(! _needsCompositingBitsUpdate)return;
  final bool oldNeedsCompositing = _needsCompositing;
  _needsCompositing = false;
  visitChildren((RenderObject child) {
    child._updateCompositingBits();
    if (child.needsCompositing)
      _needsCompositing = true;
  });
  if (isRepaintBoundary || alwaysNeedsCompositing)
    _needsCompositing = true;
  if(oldNeedsCompositing ! = _needsCompositing) markNeedsPaint(); _needsCompositingBitsUpdate =false;
}
Copy the code

flushPaint

Update the display list of all rendered objects. This function is one of the core stages of the render pipeline. Draw after the layout and before replacing the scene so that the scene is composited with the latest display list for each rendered object. For an example of how to use this function, see [RenderBinding]. Through the layout and composition operations, the data that needs to be summarized has been calculated and provided to the brush object. The RenderObject is drawn on the layer, and the render(Scene) in the Window object is used to prepare the data for this function


void flushPaint() {
  .....
  try {
    final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
    _nodesNeedingPaint = <RenderObject>[];
    // Sort the dirty nodes in reverse order (deepest first).
    /// start with the leaves
    for (RenderObject node indirtyNodes.. sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {assert(node._layer ! =null);
      if (node._needsPaint && node.owner == this) {
        if (node._layer.attached) {
          /// This step is a summary of the detailed process, which will be parsed later
          PaintingContext.repaintCompositedChild(node);
        } else{ node._skippedPaintingOnLayer(); }}}assert(_nodesNeedingPaint.isEmpty);
  } finally{... }}Copy the code

Layer

Each RenderObject corresponds at the Layer, as required

static void _repaintCompositedChild(
  RenderObject child, {
  bool debugAlsoPaintedParent = false,
  PaintingContext childContext,
}) {
  OffsetLayer childLayer = child._layer;
  if (childLayer == null) {
    child._layer = childLayer = OffsetLayer();
  } else{ childLayer.removeAllChildren(); } childContext ?? = PaintingContext(child._layer, child.paintBounds); child._paintWithContext(childContext, Offset.zero); childContext.stopRecordingIfNeeded(); }Copy the code

Below is an abstract display of each RenderObject. What it really shows is that the data for each RenderObject needs to be merged from the current layer into a single frame, which can then be parsed to start merging. The data for each RenderObject can be merged into a single plane.

Layer: 3d display

Frame: 2d data layer.png

renderView.compositeFrame()

Renderobjects have been drawn in the previous step. Each RenderObject contains a ContainerLayer object. As the Layer of each RenderObject, all that needs to be handled is that the Flutter logic has been done. Dart objects need to be converted to objects that the rendering engine can recognize:

Render (Scene Scene) in the Window object prepares the data for this function, passing the data to the underlying rendering through the Navtive call

1. Use the SceneBuilder object to create the scene

2. Extract the data from the RenderObject to build the Scene’s raw data

3. Call _window. Render (scene); Achieve real data transfer to render engine, call sky engine rendering

/// Uploads the composited layer tree to the engine.
///
/// Actually causes the output of the rendering pipeline to appear on screen.
void compositeFrame() {
  try {
    final ui.SceneBuilder builder = ui.SceneBuilder();
    final ui.Scene scene = layer.buildScene(builder);
    if (automaticSystemUiAdjustment)
      _updateSystemChrome();
    _window.render(scene);
    scene.dispose();
    assert(() {
      if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
        debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
      return true; } ()); }finally{}}Copy the code

scheduleFrame

The above analysis of the whole Scene data preparation process, in the next is to start the implementation of FlutterEngine rendering to the platform UI

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

conclusion

1. From the above analysis, we know that the Widget appears on the screen and performs four steps to display it. We split the Widow class in FlutterUI framework, and make different mixin classes to handle different callback events

2. Use runApp to associate widgets with the framework layer:

1. Bind Element to RenderObject.

2. Trigger the system to draw a frame

3. Handle different actual function BindingBaseGestureBinding respectively, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding

4. Through RenderObjectToWidgetAdapter object Element association, the RenderObject object, mainly processing data and the data in the RenderView Widget

5.Element manages Widget data

The RenderObject interface to the Flutter engine provides Layters to draw UI data

BuildContext provides an interface to access RenderObject objects in widgets

8. Find out which elements need to be redrawn

9.Pipeline looks for RenderObject objects that need to be laid out, filled, and drawn at the Layer

10. Call the method in RenderView and pass the generated data to the rendering engine with window.render(scene)

11. How is the RenderObject placed in the right place

The above analysis process is followed by the previous analysis, which analyzed FlutterEngine: Main () is called to enter the FlutterUI layer for initialization, followed by a frame that is put together by the UI layer and sent back to the FlutterEngine for processing. Next we’ll look at how the FlutterUI layer passes a UI to the Android SurfaceView for display.