Flutter的绘制流程

1. 动画与微任务阶段:

主要是处理动画及执行一系列微任务。具体是在SchedulerBinding中的handleBeginFrame函数中实现。

ini 复制代码
void handleBeginFrame(Duration rawTimeStamp) {
    ...
    try {
      // TRANSIENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.transientCallbacks;
      //切换为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 {
      //切换为midFrameMicrotasks阶段
      _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
  }

2. 构建阶段(build):

在该阶段主要是重新构建标记为"脏"的Widget节点及将需要更新的RenderObject对象标记为"脏"。

当handleBeginFrame函数执行完毕后,就会执行handleDrawFrame函数,该函数在SchedulerBinding对象初始化时会与Window相关联,所以除第一次需要主动调用外,其他时候皆是通过Window来调用该函数。

ini 复制代码
  void handleDrawFrame() {
    assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
    Timeline.finishSync(); // end the "Animate" phase
    try {
      //持久帧回调,该回调会一直存在,不会移除
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
      //当前帧绘制完成回调
      _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
      profile(() {
        _profileFrameStopwatch.stop();
        _profileFramePostEvent();
      });
      _currentFrameTimeStamp = null;
    }
  }

这里重点关注持久帧回调,该回调也是UI绘制的关键函数,是在RendererBinding对象初始化时注册的。主要函数:persistentCallbacks

scss 复制代码
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
  @override
  void initInstances() {
    super.initInstances();
    ...
    //注册持久帧回调
    addPersistentFrameCallback(_handlePersistentFrameCallback);
  }
  void _handlePersistentFrameCallback(Duration timeStamp) {
    //绘制帧
    drawFrame();
  }
  //绘制帧
  void drawFrame() {
    //对Widget进行测量、布局
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    //对Widget进行绘制
    pipelineOwner.flushPaint();
    //发送数据给GPU
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
  }
}

在WidgetsBinding中重写了drawFrame函数。在该函数中会创建新的Widget对象替换旧的Widget对象并将不需要的Element节点从树中移除。

typescript 复制代码
  @override
  void drawFrame() {
    ...
    try {
      //如果根结点存在,就重新构建Widget
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      //调用RendererBinding中的drawFrame函数
      super.drawFrame();
      //移除不再使用的Element节点
      buildOwner.finalizeTree();
    } finally {...}
    ...
  }

2.1. 重新build Widget对象:

ini 复制代码
  void buildScope(Element context, [ VoidCallback callback ]) {
    if (callback == null && _dirtyElements.isEmpty)
      return;
    try {
      //"脏"节点列表需要重新排序
      _scheduledFlushDirtyElements = true;
      ...
      //将标记为"脏"的Element节点根据深度进行排序
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      //标记为"脏"的Element节点数量
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      //遍历"脏"节点
      while (index < dirtyCount) {
        try {
          //重新构建Widget,及是否复用当前Element
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          ...
        }
        index += 1;
        //当_dirtyElements集合中的"脏"节点还未处理完毕时,又添加了新的"脏"节点
        if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
          //根据"脏"节点的深度进行排序
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          //如果当前节点的深度比新加入的"脏"节点深度要深,则需要将处理坐标指向新加入的"脏"节点
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            index -= 1;
          }
        }
      }
    } finally {
      //清除_dirtyElements中所有节点的"脏"状态
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
    }
  }

_dirtyElements是一个集合,存储了所有标记为"脏"的节点。在对其中的"脏"节点进行处理时,需要首先对集合中的"脏"节点进行排序,其排序规则如下。

● 如果"脏"节点的深度不同,则按照深度进行升序排序

● 如果"脏"节点的深度相同,则会将"脏"节点放在集合的右侧,"干净"节点则在在集合的左侧。

在排序完成后,就要遍历该集合,对其中的"脏"节点进行处理。在这里调用的是rebuild函数,通过该函数,会重新创建"脏"节点下的所有Widget对象,并根据新的Widget对象来判断是否需要重用Element对象。一般只要不是增删Widget,Element对象都会被重用,从而也就会重用RenderObject对象。由于Widget是一个非常轻量级的数据结构,所以在UI更新时做到了把性能损耗降到最低。

如果_dirtyElements中的"脏"节点还未处理完毕,就又新增了"脏"节点,那么这时候就会重新排序,保证_dirtyElements集合的左侧永远是"干净"节点,右侧永远是"脏"节点。

由于rebuild函数比较重要,这里就重点介绍一下该函数,在rebuild函数中会调用performRebuild函数,该函数是一个抽象函数,在其子类实现,而标记为"脏"的Element都是StatefulElement。所以就来StatefulElement或者其父类中查找performRebuild函数。

scala 复制代码
abstract class ComponentElement extends Element {
  ...
  @override
  void performRebuild() {
    Widget built;
    try {
      //重新创建新的`Widget`对象
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      //当构建Widget对象出错时展示的默认页面,可以修改该页面来使异常界面更友好的显示
      built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
    } finally {
      //清除"脏"标记
      _dirty = false;
    }
    try {
      //更新子Element对应的Widget对象
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      //当构建Widget对象出错时展示的默认页面
      built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
      _child = updateChild(null, built, slot);
    }
  }
}

performRebuild函数做的事很简单,就是创建新的Widget对象来替换旧的对象。上面的build函数调用的就是State类中的build函数,然后再调用Element的updateChild函数,就是更新Element对应的Widget对象。而在updateChild函数中又会调用子Element的update函数,从而调用子Element的performRebuild,然后在调用子Element的updateChild、update函数。以此类推,从而更新其所有子Element的Widget对象。

最后就是调用叶子节点的updateRenderObject函数来更新RenderObject。在更新RenderObject对象时,会根据情况来对需要重新布局及重新绘制的RenderObject对象进行标记。然后等待下一次的Vsync信号时来重新布局及绘制UI。

2.2. 标记RenderObject:

对于RenderObject对象,可以通过markNeedsLayout及markNeedsPaint来标记是否需要重新布局及重新绘制。但在当前阶段只会调用markNeedsLayout来标记需要重新布局的RenderObject对象,在下一阶段才会标记需要重新绘制的RenderObject,所以先来看markNeedsLayout函数。

java 复制代码
  void markNeedsLayout() {
    ...
    //判断布局边界是否是是当前RenderObject对象
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if (owner != null) {
        //标记当前RenderObject及其子RenderObject对象需要重新布局
        //将当前`RenderObject`添加到集合中。
        owner._nodesNeedingLayout.add(this);
        owner.requestVisualUpdate();
      }
    }
  }
  @protected
  void markParentNeedsLayout() {
    _needsLayout = true;
    final RenderObject parent = this.parent;
    if (!_doingThisLayoutWithCallback) {
      //调用父类的markNeedsLayout函数
      parent.markNeedsLayout();
    } else {
      assert(parent._debugDoingThisLayout);
    }
    assert(parent == this.parent);
  }

markNeedsLayout函数的代码实现很简单,就是不断遍历父RenderObject对象,从而找到布局边界的RenderObject对象,并将该RenderObject对象添加到集合_nodesNeedingLayout中,然后在下一阶段就从该对象开始布局。

在这里有个"布局边界"的概念,在Flutter中,可以给任意节点设置布局边界,即当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。

在重新构建build函数及标记RenderObject完成后,就进入下一阶段,开始布局。

3. 布局阶段:

计算Widget的大小及位置的确定。

在该阶段,会确定每个组件的大小及位置,相当于Android中的onMeasure+onLayout函数所实现的功能。如果是第一次调用该函数,该阶段就会遍历所有的组件,来确定其大小及位置;否则该阶段就会遍历布局边界内的所有组件,来确定其大小及位置。

当上一阶段中的buildOwner.buildScope(renderViewElement)函数执行完毕后,就会调用RendererBinding的drawFrame函数,该函数实现非常简洁。

scss 复制代码
  //绘制帧
  void drawFrame() {
    //对指定组件及其子组件进行大小测量及位置确定
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame();  
    pipelineOwner.flushSemantics();   }

其中flushLayout就是进行组件的大小及位置确定,在该函数中会遍历集合_nodesNeedingLayout并调用集合中每个对象的_layoutWithoutResize函数。

javascript 复制代码
  void flushLayout() {
    try {
      while (_nodesNeedingLayout.isNotEmpty) {
        final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = <RenderObject>[];
        for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
          //调用RenderObject对象的_layoutWithoutResize函数
          if (node._needsLayout && node.owner == this)
            node._layoutWithoutResize();
        }
      }
    } finally {...}
  }

_layoutWithoutResize函数是私有的,所以不存在重写的问题。那么就直接来看该函数。

scss 复制代码
  void _layoutWithoutResize() {
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {...}
    _needsLayout = false;
    markNeedsPaint();
  }

_layoutWithoutResize函数很简单,就直接调用了performLayout函数。

当然,RenderObject对象的size也不是随便确定的,因为在调用RenderObject的layout函数时,会传递一个继承自Constraints的对象。该对象是一个布局约束,由父传给子,子会根据该对象来决定自己的大小。

3.1. 标记RenderObject

当大小及位置确定后,就又会对RenderObject进行一次标记,这次跟上一阶段的标记大同小异,但这次是标记可绘制的RenderObject对象,然后在后面对这些对象进行重新绘制。标记可绘制的RenderObject对象是通过markNeedsPaint函数来实现的,代码如下。

csharp 复制代码
  void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      //标记需要重新绘制的RenderObject对象
      //需要绘制当前图层
      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      //没有自己的图层,与父类共用同一图层
      final RenderObject parent = this.parent;
      //遍历其父RenderObject对象
      parent.markNeedsPaint();
    } else {
      //当是RenderView时,需要自己创建新的图层
      if (owner != null)
        owner.requestVisualUpdate();
    }
  }

markNeedsPaint函数中涉及到了一个"重绘边界"的概念。在进入和走出重绘边界时,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响。当然重绘边界也可以在任何节点手动设置,但是一般不需要我们来实现,Flutter提供的控件默认会在需要设置的地方自动设置。

4. compositingBits阶段:

重绘之前的预处理操作,检查RenderObject是否需要重绘。

在组件的大小及位置确定后,就会进入当前阶段。该阶段主要是做一件事,就是将RenderObject树上新增及删除的RenderObject对象标记为"脏",方便在下一阶段对这些RenderObject对象进行重绘。具体代码实现是在flushCompositingBits函数中,该函数在Layout阶段后立即调用。

scss 复制代码
  void flushCompositingBits() {
    ...
    //将RenderObject对象按照深度进行排序
    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate && node.owner == this)
        //将RenderObject对象及其子对象标记为"脏"
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
    ...
  }

nodesNeedingCompositingBitsUpdate是一个集合,只有RenderObject对象的_needsCompositing为true时,才会添加到该集合中。在RenderObject对象创建时,_needsCompositing的值会根据isRepaintBoundary及alwaysNeedsCompositing来共同判断。

bash 复制代码
  RenderObject() {
    //isRepaintBoundary决定当前RenderObject是否与父RenderObject分开绘制,默认为false,其值在当前对象的生命周期内无法修改。也就是判断当前对象是否是绘制边界
    //alwaysNeedsCompositing为true表示当前RenderObject会一直重绘,如视频播放,默认为false
    _needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
  }

然后在向树中添加或者删除RenderObject对象时会调用adoptChild及dropChild函数,而这两个函数都会调用markNeedsCompositingBitsUpdate函数,也就在markNeedsCompositingBitsUpdate函数内完成了将当前对象添加到集合中的操作。

scss 复制代码
  //向树中添加当前节点
  @override
  void adoptChild(RenderObject child) {
    setupParentData(child);
    markNeedsLayout();
    //将当前对象的_needsCompositingBitsUpdate值标为true
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
  }
  //从树中移除当前节点
  @override
  void dropChild(RenderObject child) {
    child._cleanRelayoutBoundary();
    child.parentData.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    //将当前对象的_needsCompositingBitsUpdate值标为true
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
  }
  //
  void markNeedsCompositingBitsUpdate() {
    if (_needsCompositingBitsUpdate)
      return;
    _needsCompositingBitsUpdate = true;
    if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      if (parent._needsCompositingBitsUpdate)
        return;
      if (!isRepaintBoundary && !parent.isRepaintBoundary) {
        parent.markNeedsCompositingBitsUpdate();
        return;
      }
    }
    //将当前对象或者其父对象添加到_nodesNeedingCompositingBitsUpdate集合中
    if (owner != null)
      owner._nodesNeedingCompositingBitsUpdate.add(this);
  }

这样就会在调用flushCompositingBits函数时,就会调用_updateCompositingBits函数来判断是否将这些对象及子对象标记为"脏",然后在下一阶段进行绘制。

ini 复制代码
  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;
  }

5. 绘制阶段:

根据Widget大小及位置来绘制UI。

通过调用flushPaint函数就可以重绘已经标记的"脏"RenderObject对象及其子对象。

javascript 复制代码
  void flushPaint() {
    try {
      final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];
      //根据节点深度进行排序
      for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
        if (node._needsPaint && node.owner == this) {
          //当前对象是否与layer进行关联
          if (node._layer.attached) {
            //在图层上绘制UI
            PaintingContext.repaintCompositedChild(node);
          } else {
            //跳过UI绘制,但当前节点为"脏"的状态不会改变
            node._skippedPaintingOnLayer();
          }
        }
      }
    } finally {}
  }

flushPaint函数中,每次遍历"脏"RenderObject对象时,都会进行一次排序,避免重复绘制。然后在判断当前对象是否与Layer进行关联,如果没有关联,则无法进行绘制,但不会清除"脏"标记。下面来看repaintCompositedChild函数的实现。

scss 复制代码
  static void repaintCompositedChild(RenderObject child, { bool
    _repaintCompositedChild(
      child,
      debugAlsoPaintedParent: debugAlsoPaintedParent,
    );
  }

  static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext childContext,
  }) {
    //拿到Layer对象
    OffsetLayer childLayer = child._layer;
    if (childLayer == null) {
      //创建新的Layer对象
      child._layer = childLayer = OffsetLayer();
    } else {
      //移除Layer对象的后继节点
      childLayer.removeAllChildren();
    }
    //创建context对象
    childContext ??= PaintingContext(child._layer, child.paintBounds);
    //调用paint函数开始绘制
    child._paintWithContext(childContext, Offset.zero);
    childContext.stopRecordingIfNeeded();
  }

5.1. Layer

在该函数中主要是对Layer对象的处理,然后调用_paintWithContext函数,在_paintWithContext函数中就会调用paint这个函数,从而实现UI的绘制。至此,就完成了UI的绘制,下面再来看一个被忽略的对象------Layer。

Layer是"图层"意思,在Flutter中是最容易被忽略但又无比重要的一个类。它非常贴近底层,可以很容易的看到调用Native方法。

Layer跟其他三棵树一样,也是一棵树,有"脏"状态的标记、更新等操作。不同的是,Layer是一个双链表结构,在每个Layer对象中都会指向其前置节点与后置节点(叶子Layer的后置节点为null)。

java 复制代码
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
  //返回父节点
  @override
  ContainerLayer get parent => super.parent;

  //当前节点状态,为true表示当前节点是"脏"数据。需要重绘
  bool _needsAddToScene = true;

  //将当前节点标记为"脏"
  @protected
  @visibleForTesting
  void markNeedsAddToScene() {
    // Already marked. Short-circuit.
    if (_needsAddToScene) {
      return;
    }
    _needsAddToScene = true;
  }
  
  @protected
  bool get alwaysNeedsAddToScene => false;

  //这个是一个非常重要的东西,主要用于节点数据的缓存。存储当前节点的渲染数据,如果当前节点不需要更新,就直接拿存储的数据使用。
  @protected
  ui.EngineLayer get engineLayer => _engineLayer;
  
  //更改当前节点的数据
  @protected
  set engineLayer(ui.EngineLayer value) {
    _engineLayer = value;
      if (parent != null && !parent.alwaysNeedsAddToScene) {
        //将父节点标记需要更新的状态
        parent.markNeedsAddToScene();
      }
    }
  }
  ui.EngineLayer _engineLayer;

  //更新当前节点状态,如果_needsAddToScene为true,则将当前节点标记为"脏"
  @protected
  @visibleForTesting
  void updateSubtreeNeedsAddToScene() {
    _needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
  }

  //指向后置节点
  Layer get nextSibling => _nextSibling;
  Layer _nextSibling;

  //指向前置节点
  Layer get previousSibling => _previousSibling;
  Layer _previousSibling;
  
  //将子节点从Layer树中移除
  @override
  void dropChild(AbstractNode child) {
    if (!alwaysNeedsAddToScene) {
      markNeedsAddToScene();
    }
    super.dropChild(child);
  }
  //将当前节点添加到Layer树中
  @override
  void adoptChild(AbstractNode child) {
    if (!alwaysNeedsAddToScene) {
      markNeedsAddToScene();
    }
    super.adoptChild(child);
  }

  //将当前节点从Layer树中移除
  @mustCallSuper
  void remove() {
    parent?._removeChild(this);
  }

  
  void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]);
  
  void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
    //使用当前节点的缓存的数据
    if (!_needsAddToScene && _engineLayer != null) {
      builder.addRetained(_engineLayer);
      return;
    }
    addToScene(builder);
    //将当前节点标记为"干净"的
    _needsAddToScene = false;
  }
}

5.2. Layer节点的添加

previousSibling与nextSibling分别是Layer的前置节点与后置节点,当向Layer树中添加Layer节点时,也会将当前Layer设置为父节点的后置节点,父节点设置为当前节点的前置节点。这样,就形成了一颗树。

ini 复制代码
class ContainerLayer extends Layer {
  ...
  //将当前节点及其链表上的所有子节点都加入到Layer树中
  @override
  void attach(Object owner) {
    super.attach(owner);
    Layer child = firstChild;
    while (child != null) {
      child.attach(owner);
      child = child.nextSibling;
    }
  }
  
  //将当前节点及其链表上的所有子节点都从Layer树中移除
  @override
  void detach() {
    super.detach();
    Layer child = firstChild;
    while (child != null) {
      child.detach();
      child = child.nextSibling;
    }
  }
  //将child添加到链表中
  void append(Layer child) {
    adoptChild(child);
    child._previousSibling = lastChild;
    if (lastChild != null)
      lastChild._nextSibling = child;
    _lastChild = child;
    _firstChild ??= child;
  }
  ...
}

在上述的append函数中就将子节点添加到Layer树并加入到双链表中。在adoptChild函数中最终会调用attach函数,从而完成Layer树的添加。

5.3. Layer的状态更新

ini 复制代码
class ContainerLayer extends Layer {
  ...
  //更新Layer节点的状态。
  @override
  void updateSubtreeNeedsAddToScene() {
    super.updateSubtreeNeedsAddToScene();
    Layer child = firstChild;
    while (child != null) {
      child.updateSubtreeNeedsAddToScene();
      _needsAddToScene = _needsAddToScene || child._needsAddToScene;
      child = child.nextSibling;
    }
  }
  ...
}

updateSubtreeNeedsAddToScene函数就是更新Layer的状态,可以发现,在更新当前Layer的状态时,也会更新其所有子Layer的状态。

6. compositing阶段:

将UI数据发送给GPU处理。

该阶段主要是将更新后的数据传递给GPU。这时候调用的是compositeFrame函数,该函数很简单,就是调用了一个Native函数。

scss 复制代码
  void compositeFrame() {
    Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
    try {
      final ui.SceneBuilder builder = ui.SceneBuilder();
      final ui.Scene scene = layer.buildScene(builder);
      if (automaticSystemUiAdjustment)
        _updateSystemChrome();
      //更新后数据交给GPU处理
      _window.render(scene);
      scene.dispose();
    } finally {
      Timeline.finishSync();
    }
  }

7. semantics阶段:

与平台的辅助功能相关。

在向GPU发送数据后,Flutter还会调用flushSemantics函数。该函数与系统的辅助功能相关,一般情况下是不做任何处理。

8. finalization阶段:

主要是从Element树中移除无用的Element对象及处理绘制结束回调。

在该阶段,主要是将Element对象从树中移除及处理添加在_postFrameCallbacks集合中的回调函数。由于该回调函数是在绘制结束时调用,所以在该回调函数中,context已经创建成功。

其他:

flushLayout触发布局,将RenderObject树的dirty节点通过调用performLayout方法进行逐一布局,我们先看一下RenderPadding中的实现

ini 复制代码
@override
void performLayout() {
  _resolve();//解析padding参数
  if (child == null) {//如果没有child,直接将constraints与padding综合计算得出自己的size
    size = constraints.constrain(Size(
      _resolvedPadding.left + _resolvedPadding.right,
      _resolvedPadding.top + _resolvedPadding.bottom
    ));
    return;
  }
  final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);//将padding减去,生成新的约束innerConstraints
  child.layout(innerConstraints, parentUsesSize: true);//用新的约束去布局child
  final BoxParentData childParentData = child.parentData;
  childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);//设置childParentData的offset值
  size = constraints.constrain(Size(//将constraints与padding以及child的sieze综合计算得出自己的size
    _resolvedPadding.left + child.size.width + _resolvedPadding.right,
    _resolvedPadding.top + child.size.height + _resolvedPadding.bottom
  ));
}

可以看到RenderPadding中的布局分两种情况。如果没有child,那么就直接拿parent传过来的约束以及padding来确定自己的大小;否则就先去布局child,让后再拿parent传过来的约束和padding以及child的size来确定自己的大小。RenderPadding是典型的单child的RenderBox,我们看一下多个child的RenderBox。例如RenderFlow

ini 复制代码
@override
void performLayout() {
  size = _getSize(constraints);//直接先确定自己的size
  int i = 0;
  _randomAccessChildren.clear();
  RenderBox child = firstChild;
  while (child != null) {//遍历孩子
    _randomAccessChildren.add(child);
    final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);//获取child的约束,此方法为抽象
    child.layout(innerConstraints, parentUsesSize: true);//布局孩子
    final FlowParentData childParentData = child.parentData;
    childParentData.offset = Offset.zero;
    child = childParentData.nextSibling;
    i += 1;
  }
}

可以看到RenderFlow的size直接就根据约束来确定了,并没去有先布局孩子,所以RenderFlow的size不依赖与孩子,后面依旧是对每一个child依次进行布局。

还有一种比较典型的树尖类型的RenderBox,LeafRenderObjectWidget子类创建的RenderObject对象都是,他们没有孩子,他们才是最终需要渲染的对象,例如

arduino 复制代码
@override
void performLayout() {
  size = _sizeForConstraints(constraints);
}

非常简单就通过约束确定自己的大小就结束了。所以performLayout过程就是两点,确定自己的大小以及布局孩子。我们上面提到的都是RenderBox的子类,这些RenderObject约束都是通过BoxConstraints来完成,但是RenderSliver的子类的约束是通过SliverConstraints来完成,虽然他们对child的约束方式不同,但他们在布局过程需要执行的操作都是一致的。

复制代码

绘制

布局完成了,PipelineOwner就通过flushPaint来进行绘制

ini 复制代码
void flushPaint() {
  try {
    final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
    _nodesNeedingPaint = <RenderObject>[];
    // 对dirty nodes列表进行排序,最深的在第一位
    for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
      assert(node._layer != null);
      if (node._needsPaint && node.owner == this) {
        if (node._layer.attached) {
          PaintingContext.repaintCompositedChild(node);
        } else {
          node._skippedPaintingOnLayer();
        }
      }
    }
  } finally {}
}

PaintingContext.repaintCompositedChild(node)会调用到child._paintWithContext(childContext, Offset.zero)方法,进而调用到child的paint方法,我们来看一下第一次绘制的情况,dirty的node就应该是RenderView,跟进RenderView的paint方法

typescript 复制代码
@override
void paint(PaintingContext context, Offset offset) {
  if (child != null)
    context.paintChild(child, offset);//直接绘制child
}

自己没有什么绘制的内容,直接绘制child,再看一下RenderShiftedBox

java 复制代码
@override
void paint(PaintingContext context, Offset offset) {
  if (child != null) {
    final BoxParentData childParentData = child.parentData;
    context.paintChild(child, childParentData.offset + offset);//直接绘制child
  }
}

好像没有绘制内容就直接递归的进行绘制child,那找一个有绘制内容的吧,我们看看RenderDecoratedBox

scss 复制代码
@override
void paint(PaintingContext context, Offset offset) {
  _painter ??= _decoration.createBoxPainter(markNeedsPaint);//获取painter画笔
  final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
  if (position == DecorationPosition.background) {//画背景
    _painter.paint(context.canvas, offset, filledConfiguration);//绘制过程,具体细节再painter中
    if (decoration.isComplex)
      context.setIsComplexHint();
  }
  super.paint(context, offset);//画child,里面直接调用了paintChild
  if (position == DecorationPosition.foreground) {//画前景
    _painter.paint(context.canvas, offset, filledConfiguration);
    if (decoration.isComplex)
      context.setIsComplexHint();
  }
}

如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,看一下RenderImage

less 复制代码
@override
void paint(PaintingContext context, Offset offset) {
  if (_image == null)
    return;
  _resolve();
  paintImage(//直接绘制Image,具体细节再此方法中
    canvas: context.canvas,
    rect: offset & size,
    image: _image,
    scale: _scale,
    colorFilter: _colorFilter,
    fit: _fit,
    alignment: _resolvedAlignment,
    centerSlice: _centerSlice,
    repeat: _repeat,
    flipHorizontally: _flipHorizontally,
    invertColors: invertColors,
    filterQuality: _filterQuality
  );
}

所以基本上绘制需要完成的流程就是,如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,流程比较简单。

开始执行Dart代码,经历Layout、Paint等过程,生成一棵Layer Tree,将绘制指令保存在Layer中,接着进行栅格化和合成上屏。

相关推荐
并不会1 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、1 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜1 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师1 小时前
CSS的三个重点
前端·css
耶啵奶膘2 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^4 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie5 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具6 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端