current position:Home>Flutter drawing process analysis and code practice

Flutter drawing process analysis and code practice

2022-05-15 07:38:22A leaf floating boat

Render Tree The creation process of

RenderObject The type of

We know Element It is mainly divided into those responsible for rendering RenderObjectElement And the person in charge of the combination ComponentElement Two categories: , And create RenderObject The node is the former mount() Called in the method RenderObjectWidget.createRenderObject() Method .

This method is an abstract method , You need a subclass implementation , For different layouts Widget Created RenderObject The types are different , stay Render Tree There are two main types of RenderObject:

  • The first is RenderObject There is a lot of reference to a class in the annotation RenderBox, It's most RenderObjectWidget The corresponding RenderObject The abstract class of
/// A render object in a 2D Cartesian coordinate system.
///  In a  2D  Render objects in coordinate system 
abstract class RenderBox extends RenderObject
  • as well as Render Tree The root node RenderView
/// The root of the render tree.
/// Render Tree  The root node , Handle the guidance of the rendering pipeline and the output of the rendering tree 
///  It has a that fills the entire output surface  RenderBox  The only child node of type 
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>

Other types of RenderObject Basically for a specific layout ( Like sliding 、 list ) The implementation of the , But most of them are directly or indirectly integrated from RenderBox.

Usually a RenderBox There is only one child node ( Because it has only one child attribute ), This makes it more like a linked list as a whole . Flutter Provides ContainerRenderObjectMixin Used for those who need to store multiple child nodes RenderBox Expand , The organization of multiple child nodes also uses linked lists to connect storage nodes , Here are two common :

  • RenderStack The stack layout algorithm is implemented
  • RenderFlex Realized Flex Layout algorithm ,Column and Row All belong to Flex A variation of the

RenderView How to create

since Render Tree The root node of is RenderView, So let's see RenderView Where was it created .

adopt IDE We can find the corresponding creation reference in RendererBinding in .

/// Flutter  The engine and  Render Tree  A binder between 
mixin RendererBinding on BindingBase, ServicesBinding,
SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable

This class establishes Flutter Engine and Render Tree Relationship between , Notes describe , When Binding When it is created, it will execute initInstances() Initialize and create RenderView.

/// RendererBinding

@override
void initInstances() {
  // ...  omitted  PipelineOwner  Create and  window  Initialization code 
  //  establish  RenderView
  initRenderView();
}

/// Called automatically when the binding is created.
void initRenderView() {
  // ...
  renderView = RenderView(
    configuration: createViewConfiguration(),
    window: window);
  //  initialization  RenderView
  renderView.prepareInitialFrame();
}

We go back to Flutter App Function called at startup runApp.

runApp Will create WidgetsFlutterBinding, And implement ensureInitialized() Method .

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized() // initialization 
    ..scheduleAttachRootWidget(app) //  Create the root node of the other two trees 
    ..scheduleWarmUpFrame();
}

And this WidgetsFlutterBinding Actually by 7 individual mixin Binding Combine into , That's one of them RendererBinding, And call these mixin Binding Of initInstances() All are given to the parent class BindingBase Execute... In the construction method .

This adoption mixin Combine Binding The design of can facilitate the subsequent access to new Binding.

class WidgetsFlutterBinding extends BindingBase
  with GestureBinding, SchedulerBinding, ServicesBinding,
PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

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

abstract class BindingBase {
  /// Default abstract constructor for bindings.
  ///
  /// First calls [initInstances] to have bindings initialize their
  /// instance pointers and other state, then calls
  /// [initServiceExtensions] to have bindings initialize their
  /// observatory service extensions, if any.
  BindingBase() {
    initInstances();
    initServiceExtensions();
    developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
    developer.Timeline.finishSync();
  }
}

Initialization Association of three trees

stay ensureInitialized() Method execution is completed and Render Tree After the root node , It's called scheduleAttachRootWidget() Create the root node of the other two trees , And then Render Tree Association .

@protected
void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}

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();
  }
}

ad locum attachRootWidget() Created RenderObjectToWidgetAdapter, Its essence is RenderObjectWidget, We can see that it declares the corresponding Render Tree The node type of is RenderBox, And specify the RenderBox The parent of is RenderView.

Last call attachToRenderTree() take RenderObjectToWidgetAdapter Turn into RootRenderObjectElement And on and on Render Tree Binding .


PipelineOwner Render pipeline management

current Render Tree It's just a data structure , No rendering operation . So let's study from Render Tree What kind of process is it to the interface .

Just mentioned RenderBinding Established Flutter Engine and Render Tree Relationship between , Creating RenderView In the process of , We can notice that it also creates a PipelineOwner The object of , And setting up renderView Will also RenderView Assigned to it rootNode.

/// RendererBinding
@override
void initInstances() {
  _pipelineOwner = PipelineOwner(
    onNeedVisualUpdate: ensureVisualUpdate,
    onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
    onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
  );
}

set renderView(RenderView value) {
  _pipelineOwner.rootNode = value;
}
 Copy code 

PipelineOwner In fact, the manager of the rendering pipeline , It has... In the rendering process 3 The main method :

  1. flushLayout Update the layout information of all dirty node lists
  2. flushCompositionBits Yes, recalculate needsCompositing Update the node
  3. flushPaint Redraw all dirty nodes

this 3 The two methods are usually used together in order ,RenderBiding Will be in drawFrame() Invoke this method 3 A way

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

Then let's study this 3 What did the two methods do .

flushLayout

We know when RenderObject There are two signs :

  • _needsLayout Used to identify whether it is necessary to re Layout
  • _needsPaint Used to identify whether redrawing is required

These two properties guarantee Render Tree Key attributes of local redrawing .

When a node needs to update layout information , Would call markNeedsLayout() To reset _needsLayout, But only this process will also add the current node to the PipelineOwner Of _nodesNeedingLayout in (markNeedsPaint Will be added to _nodesNeedingPaint).

//  Keep only the main code 
void markNeedsLayout() {
  _needsLayout = true;
  if (owner != null) {
    owner!._nodesNeedingLayout.add(this);
    owner!.requestVisualUpdate();
	}
}

flushLayout() Will traverse the depth of these nodes , call RenderObject Of _layoutWithoutResize() Methods to re Layout, The final will be _needsLayout Set as false And call markNeedsPaint() Let the node need to be redrawn .

/// PipelineOwner
void flushLayout() {
  //  Keep only the main logic 
  while (_nodesNeedingLayout.isNotEmpty) {
    final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
    _nodesNeedingLayout = <RenderObject>[];
    //  Depth traversal 
    for (RenderObject node in dirtyNodes..sort(
      (RenderObject a, RenderObject b) => a.depth - b.depth)
    ) {
      if (node._needsLayout && node.owner == this)
        node._layoutWithoutResize();
    }
  }
}

/// RenderObject
@pragma('vm:notify-debugger-on-exception')
void _layoutWithoutResize() {
  try {
    performLayout(); //  Layout measurement 
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    _debugReportException('performLayout', e, stack);
  }
  _needsLayout = false;
  markNeedsPaint(); //  Let the nodes need to be redrawn 
}

Layout It's through performLayout() Method , The method is RenderObject Reserved for subclasses to implement themselves Layout Abstract methods of logic , For example, in RenderView Is implemented as follows

/// RenderView
@override
void performLayout() {
  // RenderView  Need to fill the whole screen 
  //  Use  ViewConfiguration  Of  size
  _size = configuration.size;

  if (child != null)
    //  Let the child nodes proceed under the layout constraints of the parent node  Layout
    child!.layout(BoxConstraints.tight(_size));
}

It should be noted that , Self defined RenderBox If you want to put it in a place that can contain multiple child nodes RenderBox in , for example RenderFlex and RenderStack, that Need to rewrite performLayout() To determine the layout size , Of course, we can also use another way , Use the constraints provided by the parent node to resize yourself :

@override
bool get sizedByParent => true;

@override
Size computeDryLayout(BoxConstraints constraints) {
  return constraints.smallest;
}

This method is in our next experiment 🧪 use .

flushCompositingBits

stay flushLayout() The method that will be called immediately after the method is flushCompositingBits(). This method performs a deep traversal update _nodesNeedingCompositingBitsUpdate The number of nodes in the list needsCompositing, It will call the node _updateCompositingBits() Method pair RenderObject Update some attributes of the node , Include :

  • _needsCompositing Whether to synthesize layer
  • _needsCompositingBitsUpdate Need to update _needsCompositing
/// PipelineOwner
void flushCompositingBits() {
  //  Keep only the main logic 
  _nodesNeedingCompositingBitsUpdate.sort(
    (RenderObject a, RenderObject b) => a.depth - b.depth);

  for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
    if (node._needsCompositingBitsUpdate && node.owner == this)
      node._updateCompositingBits();
  }
  _nodesNeedingCompositingBitsUpdate.clear();
  if (!kReleaseMode) {
    Timeline.finishSync();
  }
}

flushPaint

flushPaint() It's No 3 A called , Yes _nodesNeedingPaint Depth traversal of nodes in , Then call the node. PaintingContext Static method of repaintCompositedChild() Redraw RenderObject The view of .

/// PipelineOwner
void flushPaint() {
  //  Keep only the main logic 
  final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
  _nodesNeedingPaint = <RenderObject>[];
  // Sort the dirty nodes in reverse order (deepest first).
  for (final RenderObject node in dirtyNodes..sort(
    (RenderObject a, RenderObject b) => b.depth - a.depth)) {
    if (node._needsPaint && node.owner == this) {
      if (node._layerHandle.layer!.attached) {
        PaintingContext.repaintCompositedChild(node);
      } else {
        node._skippedPaintingOnLayer();
      }
    }
  }
}

In this method, through layers of calls, we will finally reach , Of the incoming node paint() Method .paint() Method is also RenderObject Abstract methods provided to subclasses to implement drawing logic . The same to RenderView As an example :

/// RenderView
@override
void paint(PaintingContext context, Offset offset) {
  if (child != null)
    context.paintChild(child!, offset);
}

because RenderView Is the root node of the whole tree , So there is no drawing logic , But all RenderObject Are all the same , If there are child nodes, they will pass PaintingContext Continue to call the of the child node paint() Methods and PaintingContext Pass it on , Until the nodes of the whole tree are drawn .


Scene synthesis and interface refresh rendering

We know Widget It's all through Canvas Drawn , So we use a custom View To analyze .

stay 《Flutter actual combat · The second edition 》 In this book , It's using CustomPainter To write custom View, By rewriting void paint(Canvas canvas, Size size); Method to get a Canvas object , Therefore, you can go to the source code of this method , Look at this. Canvas Source of object .

// custom_paint.dart
abstract class CustomPainter extends Listenable

/// Provides a canvas on which to draw during the paint phase.
///  Provides the information to be drawn in the drawing phase  Canvas
class RenderCustomPaint extends RenderProxyBox {

  void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
  	// ...
    //  Call it here  CustomPainter  Of  paint, And provide a  Canvas  object 
	painter.paint(canvas, size);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (_painter != null) {
      //  Provided here  canvas
      _paintWithPainter(context.canvas, offset, _painter!);
      _setRasterCacheHints(context);
    }
    super.paint(context, offset);
    if (_foregroundPainter != null)
      _paintWithPainter(context.canvas, offset, _foregroundPainter!);
      _setRasterCacheHints(context);
    }
  }
}

Here we can see that , We customize View The drawing operation of , By RenderCustomPaint perform , Its essence is actually a RenderBox, And the incoming Canvas The object is created by it in paint() Medium PaintingContext Provided .

Canvas And drawing storage

stay PaintingContext Lazy loading is used to create Canvas object ,PaintingContext Usually created in Render Tree When you start drawing a single subtree , The creation will be accompanied by the creation of two other objects :

  • PictureLayer Layers
  • PictureRecorder Image recorder
// object.dart
class PaintingContext extends ClipContext {
  Canvas? _canvas;

  ///  obtain  Canvas  object ,
  ///  When  _canvas  Call... When not created  [_startRecording]  Method creation 
  @override
  Canvas get canvas {
    if (_canvas == null)
      _startRecording();
    assert(_currentLayer != null);
    return _canvas!;
  }

  ///  establish  Canvas  object 
  /// -  establish  PictureLayer  Layer objects 
  /// -  establish  PictureRecorder  Image recorder 
  /// -  establish  Canvas  object 
  /// -  take  PictureLayer  Add to  ContainerLayer  Container layer 
  void _startRecording() {
    assert(!_isRecording);
    _currentLayer = PictureLayer(estimatedBounds);
    _recorder = ui.PictureRecorder();
    _canvas = Canvas(_recorder!);
    _containerLayer.append(_currentLayer!);
  }
}

establish Canvas Must pass in a PictureRecorder object , This object will record Canvas The drawing operation of , When the record is completed , By calling PictureRecord.endRecording To end the record , And get a Picture object , because Canvas Is drawn by Engine Layer. Skia The engine provides , therefore Picture Objects are also stored in Engine layer .

/// PictureRecorder
Picture endRecording() {
  if (_canvas == null)
    throw StateError('PictureRecorder did not start recording.');
  final Picture picture = Picture._();
  _endRecording(picture);
  _canvas!._recorder = null;
  _canvas = null;
  return picture;
}

void _endRecording(Picture outPicture) native 'PictureRecorder_endRecording';

Layer Tree

_startRecording() In addition to creating Canvas and PictureRecorder Outside , Also created a PictureLayer Object and add it to _containerLayer in . This _containerLayer It's actually RenderObject One of them Layer.

Layer Is used to cache the results of drawing operations (Picture) The layer of , Layers can be arranged according to rules to get images . Every RenderObject There will be one in every Layer, Stored in LayerHandle in ,Render Tree perform flushPaint After drawing , Will form a Layer Tree,Layer Tree The number of nodes will be greater than Render Tree Less , How many? RenderObject A node corresponds to only one Layer node .

Layer There are many kinds of nodes , But the most used are the following two :

  • Use PictureRecorder The node used to record the drawing operation PictureLayer,PictureLayer No child nodes , This is the most commonly used leaf node type
  • When needed and Layer When the child nodes are superimposed to obtain the image , You can use ContainerLayer, It provides append Method to connect Layer, To form a Layer Tree.

ContainerLayer There can be multiple child nodes , They are linked together in the form of a linked list , Generally not used directly ContainerLayer, It uses its subclasses OffsetLayer.

Use prepareInitialFrame() Method initialization RenderView Created Layer The type is TransformLayer , So is it OffsetLayer Subclasses of .

When creating a PaintingContext Provided by Layer Node does not belong to OffsetLayer when , Will create a OffsetLayer To replace the original Layer, As the root node of the current subtree . PaintingContext Create a new PictureLayer Will use append A new method will Layer Add nodes to this OffsetLayer in .

/// PaintingContext
static void _repaintCompositedChild(
  RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext? childContext,
  }) {
  OffsetLayer? childLayer = child._layerHandle.layer as OffsetLayer?;
  if (childLayer == null) {
    final OffsetLayer layer = OffsetLayer();
    child._layerHandle.layer = childLayer = layer;
  } else {
    childLayer.removeAllChildren();
  }
  //  Create... Here  PaintingContext
  childContext ??= PaintingContext(childLayer, child.paintBounds);
  child._paintWithContext(childContext, Offset.zero);
  //  Finish drawing end record 
  childContext.stopRecordingIfNeeded();
}

As mentioned above, if the node has children , Will pass context.paintChild() Let the child node also call _paintWithContext() Methods will PaintingContext Pass down , Continue to execute the of child nodes paint() Method to draw .

When the current layer is drawn , When the drawing is completed, it will call stopRecordingIfNeeded() To end the record drawing , And will PictureRecord Generated Picture Objects are cached to PictureLayer in .

/// PaintingContext
@protected
@mustCallSuper
void stopRecordingIfNeeded() {
  if (!_isRecording)
    return;
  _currentLayer!.picture = _recorder!.endRecording();
  _currentLayer = null;
  _recorder = null;
  _canvas = null;
}

/// PictureLayer
set picture(ui.Picture? picture) {
  markNeedsAddToScene();
  _picture?.dispose();
  _picture = picture;
}

Node drawing separation

Render Tree The drawing of is drawn from top to bottom by depth traversal , That is, after the current node is drawn, call the drawing method of the child node .

RenderObject Provides isRepaintBoundary Property to determine whether the current subtree needs to be drawn separately from the parent node , This property defaults to false, And there's no setter To make changes , Therefore, by default, one Render Tree May only generate 2 individual Layer node ( The root node of the TransformLayer And store the drawing results PictureLayout).

But in fact, we can RenderBox Subclasses of override this property , Or use RenderRepaintBoundary( its isRepaintBoundary Was rewritten as true), To separate the drawing of parent and child nodes , Draw separately from reach to generate different Layer Nodes form a star Layer Tree.

This attribute in markNeedsPaint() Methods are also used , The relevant source code is as follows :

void markNeedsPaint() {
  if (_needsPaint)
    return;
  _needsPaint = true;
  markNeedsPaintCout++;
  if (isRepaintBoundary) {
    if (owner != null) {
      owner!._nodesNeedingPaint.add(this);
      owner!.requestVisualUpdate();
    }
  } else if (parent is RenderObject) {
    final RenderObject parent = this.parent! as RenderObject;
    parent.markNeedsPaint();
  }
}
  • If isRepaintBoundary by true It means that it is drawn separately from the parent node , Add yourself to _nodesNeedingPaint In the list , In the next update, only the current subtree will be redrawn , It will not pollute the parent node .
  • If isRepaintBoundary by false Then the parent node's markNeedsPaint() To let the parent node handle , When the next update is redrawn by the parent node, execute your own drawing method to redraw .

And in the drawing process , If the child node's isRepaintBoundary by true, Represents the need to draw separately , Will end the current PictureRecorder And will generate Picture Deposit in Layer in , Then start the drawing of child nodes .

When drawing child nodes, due to PaintingContext Of Layer Has been set to null 了 , So a new PictureLayer And add to the root Layer List of child nodes of , If the child node does not need to be redrawn , Just put the of the child node directly Layer Add to root Layer List of child nodes of .

Used when adding here appendLayer() The current... Will be first Layer The node is removed from the original parent node , Then add , So don't be careful, there will be repeated additions , Because the essence of child node list is linked list , And there is usually no other... Between adding after creation and adding again Layer Node intervention , Therefore, there is no need to be careful about the movement and search efficiency when adding this method .

/// PaintingContext
void paintChild(RenderObject child, Offset offset) {
  if (child.isRepaintBoundary) {
    stopRecordingIfNeeded(); //  End the drawing of the current tree 
    _compositeChild(child, offset);
  } else {
    child._paintWithContext(this, offset);
  }
}

///  A lot of code is omitted 
void _compositeChild(RenderObject child, Offset offset) {
    // Create a layer for our child, and paint the child into it.
    if (child._needsPaint) {
      repaintCompositedChild(child, debugAlsoPaintedParent: true);
    }

    final OffsetLayer childOffsetLayer = child._layerHandle.layer! as OffsetLayer;
    childOffsetLayer.offset = offset;
    appendLayer(childOffsetLayer);
}

@protected
void appendLayer(Layer layer) {
  layer.remove(); //  Removes the current node from the parent node 
  _containerLayer.append(layer);
}

Scene rendering

We go back to RenderBinding Of drawFrame() In the method , to glance at Render Tree After drawing , How to render to the interface .

/// RenderBiding
@protected
void drawFrame() {
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
    _firstFrameSent = true;
  }
}

/// RenderView
void compositeFrame() {
  final ui.SceneBuilder builder = ui.SceneBuilder();
  //  Add layers to  scene
  final ui.Scene scene = layer!.buildScene(builder);
  //  send out  scene  to  GPU  Rendering 
  _window.render(scene);
  scene.dispose();
}

/// Layer
ui.Scene buildScene(ui.SceneBuilder builder) {
  updateSubtreeNeedsAddToScene();
  addToScene(builder); //  Abstract method , Implemented by subclasses 
  _needsAddToScene = false;
  final ui.Scene scene = builder.build();
  return scene;
}

When it is necessary to send a frame image to GPU when , Would call compositeFrame() Method , In this method, a SceneBuilder, And then through ContainerLayer.buildScene() take Layer Tree Of Picture Synthesis of a Scene.

Scene It can be understood as a scene , Is stored GPU The image object of the drawn pixel information , When adding OffsetLayer The offset of the layer will be set , When adding ContanierLayer It will traverse the child nodes to add , When adding PictureLayer Would call native Method in Engine add to Picture Into the image , When we call build The method is also from Engine obtain Scene object .

void _addPicture(double dx, double dy, Picture picture, int hints)
  native 'SceneBuilder_addPicture';

void _build(Scene outScene) native 'SceneBuilder_build';

Layer There are two properties in _needsAddToScene and _subtreeNeedsAddToScene To indicate whether you and the subtree need to be added to Scene in , When Layer If it is dirty, it needs to be synthesized into Scene, One Layer Or its subtree is synthesized into Scene after , The corresponding property will be set to false.

Scene After synthesis , Then call render Methods will Scene Send to GUP Render to the interface .

/// FlutterView
void render(Scene scene) => _render(scene, this);
void _render(Scene scene, FlutterView view) native 'PlatformConfiguration_render';

Interface refresh

Now we know that Flutter Is to call drawFrame() Method , To do it Render Tree The draw , that drawFrame() When will it be implemented ? Let's read the notes for this method .

/// This method is called by [handleDrawFrame], which itself is called
/// automatically by the engine when it is time to lay out and paint a frame.

The note States drawFrame() Will be in Engine When you need to provide a new image , Automatically handleDrawFrame() Method call , In fact, RenderBinding When initializing , Will add this method to persistentCallbacks Callback list .

/// RenderBinding
void initInstances() {
  // window  Some state change callbacks will be set during the initialization of 
  window
      ..onMetricsChanged = handleMetricsChanged
      ..onTextScaleFactorChanged = handleTextScaleFactorChanged
      ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
      ..onSemanticsAction = _handleSemanticsAction;
  // RenderView  Initialize creation 
  initRenderView();
  //  Add a callback here 
  addPersistentFrameCallback(_handlePersistentFrameCallback);
}

void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame(); //  Call frame drawing in this callback 
  _scheduleMouseTrackerUpdate();
}

/// SchedulerBinding

///  The callback methods in this list will be  handleDrawFrame  Take them out in turn and execute 
final List<FrameCallback> _persistentCallbacks = <FrameCallback>[];

///  Add callback to  _persistentCallbacks  in 
void addPersistentFrameCallback(FrameCallback callback) {
  _persistentCallbacks.add(callback);
}

handleDrawFrame() When executed , This callback will be taken from the callback list , So when the screen is refreshed, it will call drawFrame() take Render Tree Draw on the interface .

/// Engine  Call this method to provide a new frame of image 
void handleDrawFrame() {
  // PERSISTENT FRAME CALLBACKS
  _schedulerPhase = SchedulerPhase.persistentCallbacks;
  for (final FrameCallback callback in _persistentCallbacks)
    _invokeFrameCallback(callback, _currentFrameTimeStamp!);
  // ...  Keep only key code 
}

in other words , When we refresh the interface , The relevant callback work will be handed over to handleDrawFrame() To carry out , And this method except in APP When it starts , Will be first scheduleWarmUpFrame() The timer is executed once for the first demonstration , stay scheduleAttachRootWidget() Method execution , Will be registered to window.onDrawFrame As a callback for interface refresh . We use breakpoint debugging , You can see APP When starting, the registration call chain of this method is as follows :

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app) //  Register callback in advance 
    ..scheduleWarmUpFrame();
}

void attachRootWidget(Widget rootWidget) {
  //  If it's a boot frame , Visual update 
  if (isBootstrapFrame) {
    SchedulerBinding.instance!.ensureVisualUpdate();
  }
}

void ensureVisualUpdate() {
  switch (schedulerPhase) {
    case SchedulerPhase.idle:
    case SchedulerPhase.postFrameCallbacks:
      scheduleFrame(); // <-  Frame task 
      return;
    case SchedulerPhase.transientCallbacks:
    case SchedulerPhase.midFrameMicrotasks:
    case SchedulerPhase.persistentCallbacks:
      return;
  }
}

///  Here are  SchedulerBinding  The method in 
void scheduleFrame() {
  ensureFrameCallbacksRegistered(); // <-  Determine the registration of the callback 
  window.scheduleFrame(); //  Execution of request callback , Update the interface 
  _hasScheduledFrame = true;
}

@protected
void ensureFrameCallbacksRegistered() {
  window.onBeginFrame ??= _handleBeginFrame;
  window.onDrawFrame ??= _handleDrawFrame; // <-  Register callback 
}

The callback registered is actually a response to handleDrawFrame Wrapped in a shell .

void _handleDrawFrame() {
  if (_rescheduleAfterWarmUpFrame) {
    _rescheduleAfterWarmUpFrame = false;
    addPostFrameCallback((Duration timeStamp) {
      _hasScheduledFrame = false;
      scheduleFrame();
    });
    return;
  }
  handleDrawFrame();
}

window.scheduleFrame() Will send to Engine The layer initiates a request , Call... At the next appropriate time window.onDrawFrame and window.onBeginFrame Registered callbacks , To refresh the interface .

Finally, we use breakpoint debugging , When the interface is refreshed drawFrame What is the complete call chain of , Those in the green box are the methods we just talked about .

Come here , Knowledge is strung together ~


Organize the map

Let's draw a picture and sort it out , In order to make the diagram easier to see , Let's omit a billion details 🤏.


Framework Project code experiment

Of course, after understanding the relevant processes , We are directly in the Flutter Framework Experiment in the project , Write it down by yourself according to the process Render Tree Code to refresh the interface , prove 、 Also familiar with this process .

First, configure one according to the official instructions Framework development environment , And then into hello_world In the project : github.com/flutter/flu…

The experimental project is the same as the usual development, and still adopts Flutter APP Way to start , But the difference is that we don't call runApp() Method , Instead, create a Render Tree And use Canvas, Use the above process to execute our APP.

Let's try to use Canvas Draw a line , Then generate Picture Add to Sence in , And send it to GPU Rendering .

import 'dart:ui';
import 'package:flutter/material.dart';

void main() {

  final PictureRecorder pictureRecorder = PictureRecorder();
  drawLine(pictureRecorder);
  final Picture picture = pictureRecorder.endRecording();

  final SceneBuilder sceneBuilder = SceneBuilder();
  sceneBuilder.addPicture(Offset.zero, picture);
  final Scene scene = sceneBuilder.build();
  window.render(scene);
}

void drawLine(PictureRecorder recorder) {
  final Canvas canvas = Canvas(recorder);

  final Paint paint = Paint()
    ..color = Colors.white
    ..strokeWidth = 10;

  canvas.drawLine(Offset(300, 300), Offset(800, 300), paint);
}

The above code will draw a white line on the interface , Because here only render Once , So after drawing this white line , There will be no change in the interface . Now let's try to make the lines move , Through the above explanation , We know Flutter It's using window.scheduleFrame() To request a screen refresh , So we put the rendering into window.onDrawFrame in , And constantly change the line position .

import 'dart:ui';
import 'package:flutter/material.dart';

void main() {
  double dy = 300.0;

  window.onDrawFrame = () {
    final PictureRecorder pictureRecorder = PictureRecorder();
    drawLine(pictureRecorder, dy);
    if (dy < 800)
      dy++;

    final Picture picture = pictureRecorder.endRecording();

    final SceneBuilder sceneBuilder = SceneBuilder();
    sceneBuilder.addPicture(Offset.zero, picture);
    final Scene scene = sceneBuilder.build();

    //  Constantly refresh the interface 
    window.render(scene);
    window.scheduleFrame();
  };

  window.scheduleFrame();
}

void drawLine(PictureRecorder recorder, double dy) {
  final Canvas canvas = Canvas(recorder);

  final Paint paint = Paint()
    ..color = Colors.white
    ..strokeWidth = 10;

  canvas.drawLine(Offset(300, dy), Offset(800, dy), paint);
}

So you get a straight line that moves .

Next, we encapsulate the above line into a custom RenderObject, Then create your own Render Tree, And use drawFrame() Process in method : Use PipelineOwner To redraw the contaminated nodes .

void main() {
  //  Build the root node 
  final PipelineOwner pipelineOwner = PipelineOwner();
  final RenderView renderView =
      RenderView(configuration: const ViewConfiguration(), window: window);
  pipelineOwner.rootNode = renderView;
  //  initialization 
  renderView.prepareInitialFrame();

  renderView.child = MyRenderNode();

  window.onDrawFrame = () {
    callFlush(pipelineOwner);
    renderView.compositeFrame();
    window.scheduleFrame();
  };
  window.scheduleFrame();
}

void callFlush(PipelineOwner pipelineOwner) {
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
}

class MyRenderNode extends RenderBox {

  double _dy = 300;

  final Paint _paint = Paint()
    ..color = Colors.white
    ..strokeWidth = 10;

  void _drawLines(Canvas canvas, double dy) {
    canvas.drawLine(Offset(300, dy), Offset(800, dy), _paint);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    _drawLines(context.canvas, _dy);
    _dy++;
    markNeedsLayout();
  }
}

The effect of this code is the same as that above , But only one node can't see the transformation into Layer Tree The advantages of , Let's build a multi node Render Tree. We use RenderFlex To store multiple nodes , And explain with the above flushLayout() The layout size is determined by the parent node .

void main() {
  //  Build the root node 
  final PipelineOwner pipelineOwner = PipelineOwner();
  final RenderView renderView =
      RenderView(configuration: const ViewConfiguration(), window: window);
  pipelineOwner.rootNode = renderView;
  //  initialization 
  renderView.prepareInitialFrame();

  final RenderFlex flex = RenderFlex(textDirection: TextDirection.ltr);
  
  //  from  301  Start moving to  500  It's drawn  200  Time 
  double dy = 301;
  
  //  Create two leaf nodes 
  final MyRenderNode node1 = MyRenderNode(dy, Colors.white);
  final MyRenderNode node2 = MyRenderNode(dy, Colors.blue);

  renderView.child = flex;
  //  Note that this is a forward insertion 
  flex.insert(node1);
  flex.insert(node2);

  window.onDrawFrame = () {
    callFlush(pipelineOwner);
    renderView.compositeFrame();
    if (dy < 500) {
      node1.dy = ++dy;
      window.scheduleFrame();
    } else {
      print('node1 paint count: ${node1.paintCount}');
      print('node2 paint count: ${node2.paintCount}');
    }
  };

  window.scheduleFrame();
}

void callFlush(PipelineOwner pipelineOwner) {
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
}

class MyRenderNode extends RenderBox {
  MyRenderNode(this._dy, Color color) {
    _paint = Paint()
      ..color = color
      ..strokeWidth = 10;
  }

  double _dy;
  int paintCount = 0;

  set dy(double dy) {
    _dy = dy;
    markNeedsLayout();
  }

  double get dy => _dy;

  late Paint _paint;

  void _drawLines(Canvas canvas, double dy) {
    canvas.drawLine(Offset(300, dy), Offset(800, dy), _paint);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    _drawLines(context.canvas, dy);
    paintCount++;
  }

  @override
  bool get sizedByParent => true;

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return constraints.smallest;
  }
}

This code is relatively long , about MyRenderNode Modification of :

  • First we rewrite sizedByParent and computeDryLayout(), Used to determine the size during layout measurement
  • _dy Attribute added setter Method , In every revision _dy Called when the value of markNeedsLayout() To redraw the node the next time the interface refreshes
  • In addition, we have added a piantCount Attribute to record how many times the node is drawn

Next is main In the method :

  • Use RenderFlex As RenderView Child nodes of
  • Two child nodes are created and inserted into RenderFlex in
  • Every time you render , Will be modified node1 Of dy, Let him redraw ,node2 No changes will be made
  • When dy The value of has reached 500 Stop the interface refresh and print the drawing times of two nodes

The effect is as above , There will be a fixed blue line , And a moving white line . Let's look at the information printed on the console .

We found that the drawing times of both nodes are 200, This means that two nodes are redrawn each time they are rendered , According to what we said above PaintingContext and Layer Characteristics , We can quickly judge , This is because node1 and node2 Not drawn separately , Use the same Layer Caused by nodes .

because node1 After being contaminated, the parent node will also be called flex Of markNeedsPaint(), Therefore, during the drawing operation, it is drawn downward by the parent node , and node2 It's also flex Child nodes of , The whole tree will be redrawn , This is it. node2 Pollution time node1 With the reason of redrawing . ​

We are customizing RenderBox Rewriting in isRepaintBoundary attribute , And in framework Layer for ContainerLayer Add a node counting method .

/// ContainerLayer

int layerCount() {
  int count = 1; //  Add the current node 
  Layer? child = firstChild;
  while (child != null) {
    if(child is OffsetLayer)
      count += child.layerCount();
    else
      count += 1;
    child = child.nextSibling;
  }
  return count;
}
void main() {
  window.onDrawFrame = () {
    if (dy < 500) {
      node1.dy = ++dy;
      window.scheduleFrame();
    } else {
      print('node1 paint count: ${node1.paintCount}');
      print('node2 paint count: ${node2.paintCount}');
      //  Print at the end  Layer  The number of 
      print('layer count: ${renderView.layer?.layerCount()}');
    }
  };
}

class MyRenderNode extends RenderBox {
  bool _isRepaintBoundary = false;

  @override
  bool get isRepaintBoundary => _isRepaintBoundary;

  ///  Add setting method 
  set isRepaintBoundary(bool v) {
    _isRepaintBoundary = v;
  }
}

Let's demonstrate two cases first :

  1. No, two leaf nodes isRepaintBoundary Make changes

  1. take node1 Draw separately :node1.isRepaintBoundary = false;

You can see node1 Of isRepaintBoundary Set to true when , node2 It's just drawn 1 Time , Now? node2 Pollution will not lead to node1 Redrawn .

In addition, we see the second case Layer The number of nodes is 4, Why would it be 4 Well ?

Think back to the introduction PaintingContext Provide when creating Layout The requirements of :

When offered to PaintingContext Of Layer Node does not belong to OffsetLayer when , Will create a OffsetLayer To replace the original Layer, As the root node of the current subtree .

If we debug the program , You can find , Although it is because of node1、node2 The order of insertion , But the actual insert() The method is to insert it forward , stay flex in node2 Is in node1 In front of , therefore node2 Will draw first .

because node2 There is no setting to draw separately , Therefore, it will follow the normal process and flex Draw in the same PictureRecorder Generate a PictureLayer To add to TransformLayer in .

node2 After drawing, start drawing node1. Because we will node1 Set to draw separately , So draw node1 It will start drawing again as a subtree , Then... Will be called again _repaintCompositedChild() Method , Create a new PaintingContext To pass on , At this time due to node1 It's a leaf node , It doesn't come with OffsetLayer node , So a new OffsetLayer to PaintingConext, And then draw .

draw node 1 Generated on PictureLayer Add to this OffsetLayout in , When you're done, you'll OffsetLayout Add to RenderView Of TransformLayer in .

So the first 2 In this case, you will get 4 individual Layer node , Corresponding Layer Here is the following :

Let's modify the counting method , Let it print the hierarchy and node type currently traversed .

int layerCount() {
  int deep = 0;
  print('$deep ==> root is [${this.runtimeType}]');
  return _layerCount(deep + 1);
}

int _layerCount(int deep) {
  int count = 1; //  Add the current node 
  Layer? child = firstChild;
  while (child != null) {
    print('$deep ==> child is [${child.runtimeType}]');
    if(child is OffsetLayer)
      count += child._layerCount(deep + 1);
    else
      count += 1;
    child = child.nextSibling;
  }
  return count;
}

You can see that it is the same as the transformation diagram we drew . If we were to node1 and node2 In exchange , To add node2 Add again node1, send node1 Draw first , So what will the result be ?

flex.insert(node2);
flex.insert(node1);

You can see that it is still 4 individual Layer node ,node2 Generated PictureLayer Is still TransformLayer Child nodes of .

Let's see RenderFlex How to draw child nodes , We enter... Through debugging RenderFlex Of paint() Method , You can see that it calls paintDefault(), That is, the traversal is carried out and the call is made in turn At present PaintingContext Of paintChild() Draw child nodes .

void defaultPaint(PaintingContext context, Offset offset) {
  ChildType? child = firstChild;
  //  Traverse the child nodes to draw 
  while (child != null) {
    final ParentDataType childParentData = child.parentData! as ParentDataType;
    context.paintChild(child, childParentData.offset + offset);
    child = childParentData.nextSibling;
  }
}

RenderFlex When looping drawing , The parent node and the following child nodes use the same PaintingContex. because node1 Is drawn separately , So a new PaintingContext and OffsetLayer, But draw node2 Still use Parent node PaintingContext, therefore flex and node2 Will generate a PictureLayer Add to the root node .

Conclusion

This is the end of the article , We can see now ,Flutter A very important reason for high performance , It is in resource reuse and avoiding unnecessary calculation , Did a lot of thinking .

The reason for studying this part , Because this part is Flutter Framework The layer is closest to Engine Layer content , For future research Flutter and Android The two Engine The similarities and differences of layers will be of great help .

copyright notice
author[A leaf floating boat],Please bring the original link to reprint, thank you.
https://en.chowdera.com/2022/131/202205102132518149.html

Random recommended