Flutter的布局和绘制详解 ---布局和绘制

上一篇文章我们讲了Flutter的三棵树是如何创建并建立联系的,有了这三棵树,本篇文章将分析布局和绘制的流程。

1.布局与绘制入口

我们摘抄一段《Flutter实战·第二版》的原文:

一次绘制过程,我们称其为一帧(frame)。我们之前说的 Flutter 可以实现60fps(Frame Per-Second)就是指一秒钟最多可以触发 60 次重绘,FPS 值越大,界面就越流畅。这里需要说明的是 Flutter中 的 frame 概念并不等同于屏幕刷新帧(frame),因为Flutter UI 框架的 frame 并不是每次屏幕刷新都会触发,这是因为,如果 UI 在一段时间不变,那么每次屏幕刷新都重新走一遍渲染流程是不必要的,因此,Flutter 在第一帧渲染结束后会采取一种主动请求 frame 的方式来实现只有当UI可能会改变时才会重新走渲染流程。

由此我们可知,Flutter其实并不是完全根据屏幕刷新频率来发送重绘信号进行绘制的。事实上,通常我们在实际中进行绘制的主要有两种场景:

  1. App启动,即runApp()
  2. 数据变化时发起重绘,比如:setState()

1.1.runApp()

我们先回顾上一篇文章说到的启动流程:程序入口runApp()的实现:

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

在这个过程中创建了Flutter的三棵树,并将三棵树进行绑定。最终调用scheduleWarmUpFrame()触发首帧的绘制。scheduleWarmUpFrame()会执行到handleDrawFrame()方法,核心实现是在RendererBindingdrawFrame()方法中:

  @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;
    }
  }

即真正的每一帧绘制过程,这个过程我们之后分析。由此我们可知在启动App的时候,不发送Vsync信号即可触发首帧的绘制。

1.2.setState()

另一个常用场景就是当数据发生变化时,我们调用setState()进行更新重绘。我们看一下setState()的实现:

  @protected
  void setState(VoidCallback fn) {
//...
    _element!.markNeedsBuild();
  }

核心实现就是_element!.markNeedsBuild()

 void markNeedsBuild() {
    assert(_lifecycleState != _ElementLifecycle.defunct);
    if (_lifecycleState != _ElementLifecycle.active) {
      return;
    }
//...
    if (dirty) {
      return;
    }
    _dirty = true;
    owner!.scheduleBuildFor(this);
  }

Widget所对应的Element标记为dirty。这个owner就是我们上一篇文章中提到的在WidgetsBinding初始化时创建的BuildOwner对象,我们回顾一下它的创建:

@override
  void initInstances() {
    super.initInstances();
//...
    _buildOwner = BuildOwner();
    buildOwner!.onBuildScheduled = _handleBuildScheduled;
//...
  }

buildOwner持有个onBuildScheduledcallBack,它的实现是:

void _handleBuildScheduled() {
//...
    ensureVisualUpdate();
  }

其中ensureVisualUpdate()SchedulerBinding的方法,其实现如下:

void ensureVisualUpdate() {
    switch (schedulerPhase) {
      case SchedulerPhase.idle:
      case SchedulerPhase.postFrameCallbacks:
        scheduleFrame();
        return;
      case SchedulerPhase.transientCallbacks:
      case SchedulerPhase.midFrameMicrotasks:
      case SchedulerPhase.persistentCallbacks:
        return;
    }
  }
  void scheduleFrame() {
    if (_hasScheduledFrame || !framesEnabled) {
      return;
    }
    assert(() {
      if (debugPrintScheduleFrameStacks) {
        debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
      }
      return true;
    }());
    ensureFrameCallbacksRegistered();
    platformDispatcher.scheduleFrame();
    _hasScheduledFrame = true;
  }
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';

实际上就是请求新的frame。那么onBuildScheduled是什么时候被调用的呢?回顾完BuildOwner我们再回到setState(),看看owner!.scheduleBuildFor(this)的实现:

void scheduleBuildFor(Element element) {
//...
    if (element._inDirtyList) {
//...
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled!();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
//...
  }

我们看到了一个熟悉的方法执行:BuildOwneronBuildScheduled被触发了。也就是请求了下一帧的frame等待被重绘,再把Element添加到dirty element列表中等待下一帧的rebuild
setState()的流程也分析完了,我们总结一下:在启动App的时候,不会发送Vsync信号,直接进行首帧的绘制。而在数据发生变化需要重绘,比如 setState()被触发时,会将Widget对应的Element标记为dirty,然后发送Vsync信号等待被重绘。
在分析具体的绘制流程之前,我们分析一下发送Vsync信号后是如何触发重绘流程的。

2.SchedulerBinding

我们回到WidgetsFlutterBinding,上一篇文章介绍过,WidgetsFlutterBinding混合了各种Binding。这些Binding基本都是监听并处理Window对象的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。其中SchedulerBinding监听刷新事件,绑定Framework绘制调度。
我们回到上一章节提到的scheduleFrame()的实现,它是SchedulerBinding的方法:

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

在执行platformDispatcher.scheduleFrame()发起重绘之前调用了ensureFrameCallbacksRegistered()

  @protected
  void ensureFrameCallbacksRegistered() {
    platformDispatcher.onBeginFrame ??= _handleBeginFrame;
    platformDispatcher.onDrawFrame ??= _handleDrawFrame;
  }

platformDispatcher注册了onBeginFrameonDrawFrame回调。我们追踪一下onBeginFrameonDrawFrame的调用时机:

FrameCallback? get onBeginFrame => _onBeginFrame;
  set onBeginFrame(FrameCallback? callback) {
    _onBeginFrame = callback;
    _onBeginFrameZone = Zone.current;
  }
VoidCallback? get onDrawFrame => _onDrawFrame;
  set onDrawFrame(VoidCallback? callback) {
    _onDrawFrame = callback;
    _onDrawFrameZone = Zone.current;
  }

get方法是在SingletonFlutterWindow中被执行的:

  FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;
  VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;

SingletonFlutterWindowonBeginFrameonDrawFrame就是当Engine层收到Vsync信号时被回调的,也就是说发送了重绘信号后,将会在此时被执行。我们回到SchedulerBindingensureFrameCallbacksRegistered()方法:

  @protected
  void ensureFrameCallbacksRegistered() {
    platformDispatcher.onBeginFrame ??= _handleBeginFrame;
    platformDispatcher.onDrawFrame ??= _handleDrawFrame;
  }

追踪_handleBeginFrame_handleDrawFrame的实现:
_handleBeginFrame:

  void _handleBeginFrame(Duration rawTimeStamp) {
    if (_warmUpFrame) {
//...
      _rescheduleAfterWarmUpFrame = true;
      return;
    }
    handleBeginFrame(rawTimeStamp);
  }
void handleBeginFrame(Duration? rawTimeStamp) {
    _frameTimelineTask?.start('Frame');
    _firstRawTimeStampInEpoch ??= rawTimeStamp;
    _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
    if (rawTimeStamp != null) {
      _lastRawTimeStamp = rawTimeStamp;
    }
//...
    _hasScheduledFrame = false;
    try {
      // TRANSIENT FRAME CALLBACKS
      _frameTimelineTask?.start('Animate');
      _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;
    }
  }

首先记录了一下绘制初始的时间戳,然后遍历_transientCallbacks逐一进行回调。_transientCallbacks是执行“临时”回调任务,”临时“回调任务只能被执行一次,执行后会被移出”临时“任务队列。典型的代表就是动画回调会在该阶段执行。
_handleDrawFrame

void _handleDrawFrame() {
//...
    handleDrawFrame();
  }
void handleDrawFrame() {
    assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
    _frameTimelineTask?.finish(); // end the "Animate" phase
    try {
      // PERSISTENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (final FrameCallback callback in _persistentCallbacks) {
        _invokeFrameCallback(callback, _currentFrameTimeStamp!);
      }

      // POST-FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.postFrameCallbacks;
      final List<FrameCallback> localPostFrameCallbacks =
          List<FrameCallback>.of(_postFrameCallbacks);
      _postFrameCallbacks.clear();
      for (final FrameCallback callback in localPostFrameCallbacks) {
        _invokeFrameCallback(callback, _currentFrameTimeStamp!);
      }
    } finally {
//...
    }
  }

它遍历了persistentCallbackpostFrameCallbacks,并逐一执行。其中persistentCallback执行一些持久的任务(每一个frame都要执行的任务),比如渲染管线(构建、布局、绘制)就是在该任务队列中执行的。postFrameCallbacks在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和请求新的frame。由此可知persistentCallback就是具体布局绘制的地方,那么persistentCallback是在哪里注册的呢?我们回顾一下上一篇文章的RendererBinding的初始化:

@override
  void initInstances() {
    super.initInstances();
    _instance = this;
    _pipelineOwner = PipelineOwner(
      onNeedVisualUpdate: ensureVisualUpdate,
      onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
      onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
    );
    platformDispatcher
      ..onMetricsChanged = handleMetricsChanged
      ..onTextScaleFactorChanged = handleTextScaleFactorChanged
      ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
      ..onSemanticsAction = _handleSemanticsAction;
    initRenderView();
    _handleSemanticsEnabledChanged();
    assert(renderView != null);
    addPersistentFrameCallback(_handlePersistentFrameCallback);
    initMouseTracker();
    if (kIsWeb) {
      addPostFrameCallback(_handleWebFirstFrame);
    }
  }

initInstances()时会调用addPersistentFrameCallback(_handlePersistentFrameCallback)方法进行注册,而_handlePersistentFrameCallback的实现如下:

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

执行了drawFrame()

  @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;
    }
  }

好到此为止当数据发生变化,比如执行setState()时发送Vsync信号请求重绘也调到了drawFrame()方法。真正的布局和绘制是由pipelineOwner这个渲染管道进行处理的。接下来的章节才要开始我们的正题:Flutter的布局和绘制流程。

3.flushLayout()

flushLayout确定每一个组件的布局信息(大小和位置)。我们先来看它的实现:

void flushLayout() {
    if (!kReleaseMode) {
      Map<String, String>? debugTimelineArguments;
//...
    try {
      while (_nodesNeedingLayout.isNotEmpty) {
        assert(!_shouldMergeDirtyNodes);
        final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = <RenderObject>[];
        dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
        for (int i = 0; i < dirtyNodes.length; i++) {
          if (_shouldMergeDirtyNodes) {
            _shouldMergeDirtyNodes = false;
            if (_nodesNeedingLayout.isNotEmpty) {
              _nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length));
              break;
            }
          }
          final RenderObject node = dirtyNodes[I];
          if (node._needsLayout && node.owner == this) {
            node._layoutWithoutResize();
          }
        }
        _shouldMergeDirtyNodes = false;
      }
    } finally {
//...
    }
  }

_nodesNeedingLayoutRenderObject列表,表示需要被重新布局的节点。遍历_nodesNeedingLayout,并执行node._layoutWithoutResize()方法。
那么这个_nodesNeedingLayout是如何生成的呢?
通过跟踪代码,我们发现时RenderObject调用其markNeedsLayout()方法为_nodesNeedingLayout添加数据的:

void markNeedsLayout() {
//...
    if (_relayoutBoundary == null) {
      _needsLayout = true;
      if (parent != null) {
        markParentNeedsLayout();
      }
      return;
    }
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if (owner != null) {
//...
        owner!._nodesNeedingLayout.add(this);
        owner!.requestVisualUpdate();
      }
    }
  }

这段代码的目的是通过找到_relayoutBoundary来确定需要重新被布局的节点。如果自己已经是_relayoutBoundary,那么执行owner!._nodesNeedingLayout.add(this)将自己添加到_nodesNeedingLayout中;否则只要有parent,那么就调用markParentNeedsLayout()到父节点去寻找_relayoutBoundary:

  @protected
  void markParentNeedsLayout() {
    assert(_debugCanPerformMutations);
    _needsLayout = true;
    assert(this.parent != null);
    final RenderObject parent = this.parent! as RenderObject;
    if (!_doingThisLayoutWithCallback) {
      parent.markNeedsLayout();
    } else {
      assert(parent._debugDoingThisLayout);
    }
    assert(parent == this.parent);
  }

这是个递归的过程,parent会调用parent.markNeedsLayout(),目的就是找到_relayoutBoundary添加到_nodesNeedingLayout
那么这个_relayoutBoundary是如何确定的呢?之后再进行说明。
另外markNeedsLayout()还有什么调用时机呢?通过追踪,在RenderObjectattach()方法会执行markNeedsLayout()

  @override
  void attach(PipelineOwner owner) {
    assert(!_debugDisposed);
    super.attach(owner);
    if (_needsLayout && _relayoutBoundary != null) {
      _needsLayout = false;
      markNeedsLayout();
    }
    if (_needsCompositingBitsUpdate) {
      _needsCompositingBitsUpdate = false;
      markNeedsCompositingBitsUpdate();
    }
    if (_needsPaint && _layerHandle.layer != null) {
      _needsPaint = false;
      markNeedsPaint();
    }
    if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
      _needsSemanticsUpdate = false;
      markNeedsSemanticsUpdate();
    }
  }

我们回到flushLayout(),看看node._layoutWithoutResize()的实现:

void _layoutWithoutResize() {
    assert(_relayoutBoundary == this);
    RenderObject? debugPreviousActiveLayout;
//...
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
//...
    _needsLayout = false;
    markNeedsPaint();
  }

主要执行了两个方法:performLayout()markNeedsPaint()。在布局中最重要的实现就是performLayout()的实现。我们先来看performLayout(),它是RenderObject的一个抽象方法,需要子类来实现。
performLayout()需要做这么几件事:

  1. 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
  2. 子节点根据约束信息确定自己的大小(size)。
  3. 父节点根据特定布局规则(不同布局组件会有不同的布局算法)确定每一个子节点在父节点布局空间中的位置,用偏移 offset 表示。
  4. 递归整个过程,确定出每一个节点的大小和位置。

我们看个例子,比如说Align这个控件。Align继承SingleChildRenderObjectWidget,它开放了createRenderObject()方法由子类实现,返回一个RenderObject对象。我们看看Align的实现:

  @override
  RenderPositionedBox createRenderObject(BuildContext context) {
    return RenderPositionedBox(
      alignment: alignment,
      widthFactor: widthFactor,
      heightFactor: heightFactor,
      textDirection: Directionality.maybeOf(context),
    );
  }

RenderPositionedBoxperformLayout()实现如下:

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
    final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

    if (child != null) {
      child!.layout(constraints.loosen(), parentUsesSize: true);
      size = constraints.constrain(Size(
        shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
        shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
      ));
      alignChild();
    } else {
//...
    }
  }
  @protected
  void alignChild() {
    _resolve();
//...
    final BoxParentData childParentData = child!.parentData! as BoxParentData;
    childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
  }

只考虑Alignchild的情况:

  1. constraints即约束条件,限制子节点的最大和最小宽高。
  2. 先对child进行layout(),并获取childsize
  3. 根据child的大小确定自身的大小size
  4. 执行alignChild()算出childAlign组件中的偏移,然后将这个偏移保存在childparentData中,parentData在绘制阶段会用到。

我们分析一下对child进行layout()layout()的实现:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    if (!kReleaseMode && debugProfileLayoutsEnabled) {
      Map<String, String> debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent;
//...
    final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
    final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;
//...

    if (!_needsLayout && constraints == _constraints) {
//...

      if (relayoutBoundary != _relayoutBoundary) {
        _relayoutBoundary = relayoutBoundary;
        visitChildren(_propagateRelayoutBoundaryToChild);
      }

      if (!kReleaseMode && debugProfileLayoutsEnabled)
        Timeline.finishSync();
      return;
    }
    _constraints = constraints;
    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {

      visitChildren(_cleanChildRelayoutBoundary);
    }
    _relayoutBoundary = relayoutBoundary;
//...
    if (sizedByParent) {
//...
      try {
        performResize();
//...
      } catch (e, stack) {
        _debugReportException('performResize', e, stack);
      }
//...
    }
    RenderObject? debugPreviousActiveLayout;
//...
    try {
      performLayout();
      markNeedsSemanticsUpdate();
//...
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
//...
    _needsLayout = false;
    markNeedsPaint();

    if (!kReleaseMode && debugProfileLayoutsEnabled)
      Timeline.finishSync();
  }

这段代码非常核心,首先就确定了之前我们遗留的relayoutBoundary的疑问:

final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;

relayoutBoundary确定的原则是“组件自身的大小变化不会影响父组件”,如果一个组件满足以下四种情况之一,则它便是relayoutBoundary

  1. parentUsesSize,即当前组件父组件的大小不依赖当前组件大小时。
  2. sizedByParent,组件的大小只取决于父组件传递的约束,而不会依赖子组件的大小。
  3. constraints.isTight,父组件传递给自身的约束是一个严格约束。这种情况下即使自身的大小依赖后代元素,但也不会影响父组件。
  4. parent is! RenderObject,说明当前组件是根组件,即RenderView,它没有parent。

如果当前组件不是relayoutBoundary,那么就把父组件的_relayoutBoundary赋值给自己的relayoutBoundary
之后如果_needsLayoutfalse,且constraints约束条件没有变化,则无需重新布局直接return
relayoutBoundary赋值给自己的成员变量_relayoutBoundary
如果是sizedByParent的,则执行performResize()方法。performResize()是个抽象方法,需要子类进行实现,我们再摘抄一段《Flutter实战·第二版》的原文:

performLayout中确定当前组件的大小时通常会依赖子组件的大小,如果sizedByParenttrue,则当前组件的大小就不依赖子组件大小了,为了逻辑清晰,Flutter 框架中约定,当sizedByParenttrue时,确定当前组件大小的逻辑应抽离到 performResize()中,这种情况下performLayout主要的任务便只有两个:对子组件进行布局和确定子组件在当前组件中的布局起始位置偏移。

回到layout()方法,执行performLayout()performLayout()又回调用childlayout()方法形成递归。之后将_needsLayout设置为false,然后调用markNeedsPaint()请求重绘:

void markNeedsPaint() {
//...
    if (_needsPaint) {
      return;
    }
    _needsPaint = true;
    // If this was not previously a repaint boundary it will not have
    // a layer we can paint from.
    if (isRepaintBoundary && _wasRepaintBoundary) {
//...
      if (owner != null) {
        owner!._nodesNeedingPaint.add(this);
        owner!.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      parent.markNeedsPaint();
      assert(parent == this.parent);
    } else {
//...
      if (owner != null) {
        owner!.requestVisualUpdate();
      }
    }
  }

大概的逻辑是如果自己是isRepaintBoundary,那么将自己添加到_nodesNeedingPaint列表中,_nodesNeedingPaint将在绘制的时候使用;否则如果有父的话调用父的markNeedsPaint()形成递归。
到此为止,flushLayout()到实现就分析完毕了。我们继续分析flushCompositingBits()

4.flushCompositingBits()

4.1.Layer

Layer需要拿出来单独写一篇文章去讲,在此只是介绍一下,以便更好的理解绘制过程。
Flutter的绘制会将RenderObject树转成Layer树,Layer的组成由上一章节提到的RenderObject中的isRepaintBoundary来决定。
Layer有两种类型。

  1. 容器类Layer:最常用的是OffsetLayer,通常情况下一个repaintBoundary就对应一个OffsetLayer。比方说每一个路由都封装了一个repaintBoundary,那么每一个路由就在一个单独的Layer里。另外容器类 Layer可以对其子Layer整体做一些变换效果,比如剪裁效果(ClipRectLayerClipRRectLayerClipPathLayer)、过滤效果(ColorFilterLayerImageFilterLayer)、矩阵变换(TransformLayer)、透明变换(OpacityLayer)等。
  2. 绘制类Layer:最常用的是PictureLayer,比方说我们常用的TextImage组件等就在PictureLayer里。另外还有TextureLayerPlatformViewLayer主要用于native相关的绘制。

Layer的作用一个是为了在不同的frame下复用绘制的产物,另外就是确定绘制边界缩小绘制的区域。
关于Layer在此就不再做展开了,有空的时候会再写一篇认真分析一下Layer。我们回到flushCompositingBits()继续分析。

4.2.flushCompositingBits()

  void flushCompositingBits() {
    if (!kReleaseMode) {
      Timeline.startSync('UPDATING COMPOSITING BITS');
    }
    _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();
    }
  }

遍历_nodesNeedingCompositingBitsUpdate列表,并执行_updateCompositingBits()方法。它的作用是标记需要被合成的Layer所对应的对象。

  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 a node was previously a repaint boundary, but no longer is one, then
    // regardless of its compositing state we need to find a new parent to
    // paint from. To do this, we mark it clean again so that the traversal
    // in markNeedsPaint is not short-circuited. It is removed from _nodesNeedingPaint
    // so that we do not attempt to paint from it after locating a parent.
    if (!isRepaintBoundary && _wasRepaintBoundary) {
      _needsPaint = false;
      _needsCompositedLayerUpdate = false;
      owner?._nodesNeedingPaint.remove(this);
      _needsCompositingBitsUpdate = false;
      markNeedsPaint();
    } else if (oldNeedsCompositing != _needsCompositing) {
      _needsCompositingBitsUpdate = false;
      markNeedsPaint();
    } else {
      _needsCompositingBitsUpdate = false;
    }
  }

递归遍历子树确定如果每一个节点的_needsCompositing值,在绘制时只需要判断一下当前的needsCompositing 就能知道子树是否存在待合成的layer了。那么什么情况下Layer要被合成呢?我们回过头看一下_nodesNeedingCompositingBitsUpdate是怎么来的。

void markNeedsCompositingBitsUpdate() {
    assert(!_debugDisposed);
    if (_needsCompositingBitsUpdate) {
      return;
    }
    _needsCompositingBitsUpdate = true;
    if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      if (parent._needsCompositingBitsUpdate) {
        return;
      }

      if ((!_wasRepaintBoundary || !isRepaintBoundary) && !parent.isRepaintBoundary) {
        parent.markNeedsCompositingBitsUpdate();
        return;
      }
    }
    // parent is fine (or there isn't one), but we are dirty
    if (owner != null) {
      owner!._nodesNeedingCompositingBitsUpdate.add(this);
    }
  }

调用markNeedsCompositingBitsUpdate()的主要场景是当子节点会向Layer树中添加新的绘制类Layer时,则父级的变换类组件中就需要合成Layer。这个怎么理解呢?我们还是摘抄一个《Flutter实战·第二版》的实例:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomRotatedBox(
      child: RepaintBoundary( // 添加一个 RepaintBoundary
        child: Text(
          "A",
          textScaleFactor: 5,
        ),
      ),
    ),
  );
}

其中CustomRotatedBox这个控件的作用是将child旋转90度。它的Layer结构如下图所示:

截屏2023-01-13 14.58.48.png

执行上面的代码,会发现Text("A")并没有旋转90度,原因是CustomRotatedBox中进行旋转变换的canvas对应的是PictureLayer1,而 Text("A") 的绘制是使用的PictureLayer2对应的canvas,他们属于不同的 Layer。可以发现父子的PictureLayer "分离了",所以CustomRotatedBox也就不会对Text("A")起作用。解决这个问题的方法是:

  1. 创建一个TransformLayer(记为TransformLayer1) 添加到Layer树中,接着创建一个新的PaintingContextTransformLayer1绑定。
  2. 子节点通过这个新的PaintingContext去绘制。

如下图所示:

截屏2023-01-13 15.06.52.png

这其实就是一个重新Layer合成的过程:创建一个新的ContainerLayer,然后将该ContainerLayer传递给子节点,这样后代节点的Layer必然属于ContainerLayer,那么给这个ContainerLayer做变换就会对其全部的子孙节点生效。
回到我们之前的代码,所以在flushCompositingBits()过程中其实就是给这些TransformLayer去打_needsCompositing标记的,为之后的Layer合成做准备。
到此为止,flushCompositingBits()到实现就分析完毕了。我们继续分析flushPaint()

5.flushPaint()

代码如下:

void flushPaint() {
    if (!kReleaseMode) {
      Map<String, String>? debugTimelineArguments;
//...
    }
    try {
//...
      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)) {
        assert(node._layerHandle.layer != null);
        if ((node._needsPaint || node._needsCompositedLayerUpdate) && node.owner == this) {
          if (node._layerHandle.layer!.attached) {
            assert(node.isRepaintBoundary);
            if (node._needsPaint) {
              PaintingContext.repaintCompositedChild(node);
            } else {
              PaintingContext.updateLayerProperties(node);
            }
          } else {
            node._skippedPaintingOnLayer();
          }
        }
      }
      assert(_nodesNeedingPaint.isEmpty);
    } finally {
//...
    }
  }

遍历_nodesNeedingPaint列表,逐一执行PaintingContext.repaintCompositedChild(node)方法:

  static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
    assert(child._needsPaint);
    _repaintCompositedChild(
      child,
      debugAlsoPaintedParent: debugAlsoPaintedParent,
    );
  }
  static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext? childContext,
  }) {
//...
    OffsetLayer? childLayer = child._layerHandle.layer as OffsetLayer?;
    if (childLayer == null) {
//...
      final OffsetLayer layer = child.updateCompositedLayer(oldLayer: null);
      child._layerHandle.layer = childLayer = layer;
    } else {
      assert(debugAlsoPaintedParent || childLayer.attached);
      Offset? debugOldOffset;
//...
      childLayer.removeAllChildren();
      final OffsetLayer updatedLayer = child.updateCompositedLayer(oldLayer: childLayer);
//...
    }
    child._needsCompositedLayerUpdate = false;
//...
    childContext ??= PaintingContext(childLayer, child.paintBounds);
    child._paintWithContext(childContext, Offset.zero);
//...
    childContext.stopRecordingIfNeeded();
  }

由于被重绘的部分一定是repaintBoundary,所以会持有一个OffsetLayer,再创建一个PaintingContext对象,之后调用child._paintWithContext(childContext, Offset.zero)进行处理:

void _paintWithContext(PaintingContext context, Offset offset) {
//...
    if (_needsLayout) {
      return;
    }
    if (!kReleaseMode && debugProfilePaintsEnabled) {
      Map<String, String>? debugTimelineArguments;
//...
    RenderObject? debugLastActivePaint;
//...
    _needsPaint = false;
    _needsCompositedLayerUpdate = false;
    _wasRepaintBoundary = isRepaintBoundary;
    try {
      paint(context, offset);
//...
    } catch (e, stack) {
      _debugReportException('paint', e, stack);
    }
//...
  }

重要的paint()来了,paint()是个抽象方法,由子类进行实现。paint()的实现思路通常是这样的:如果是容器组件,要绘制自己和child,如果不是容器类组件,则绘制自己(比如Image)。自身的绘制就是在context.canvas上进行绘制。

void paint(PaintingContext context, Offset offset) {
  // ...自身的绘制
  if(hasChild){ //如果该组件是容器组件,绘制子节点。
    context.paintChild(child, offset)
  }
  //...自身的绘制
}

我们看看context.paintChild(child, offset)的实现:

  void paintChild(RenderObject child, Offset offset) {
//...

    if (child.isRepaintBoundary) {
      stopRecordingIfNeeded();
      _compositeChild(child, offset);
    } else if (child._wasRepaintBoundary) {
      assert(child._layerHandle.layer is OffsetLayer);
      child._layerHandle.layer = null;
      child._paintWithContext(this, offset);
    } else {
      child._paintWithContext(this, offset);
    }
  }

先判断child是不是repaintBoundary,如果是且需要被合成的话,那么先将childLayer进行合成,否则的话,递归执行child._paintWithContext(this, offset),进行绘制。
flushPaint()就梳理完成了,还差最后一个方法:renderView.compositeFrame()

5.compositeFrame()

先看代码:

void compositeFrame() {
    if (!kReleaseMode) {
      Timeline.startSync('COMPOSITING');
    }
    try {
      final ui.SceneBuilder builder = ui.SceneBuilder();
      final ui.Scene scene = layer!.buildScene(builder);
      if (automaticSystemUiAdjustment) {
        _updateSystemChrome();
      }
      _window.render(scene);
      scene.dispose();
//...
    } finally {
      if (!kReleaseMode) {
        Timeline.finishSync();
      }
    }
  }

它的逻辑是:先通过Layer构建Scene,最后再通过window.render API 来渲染。这个过程我们就不分析了,总而言之就是将Layer树中每一个Layer传给Skia进行绘制。

6.总结

通过《Flutter的布局和绘制详解 ---三棵树》和《Flutter的布局和绘制详解 ---布局和绘制》这两篇文章的梳理,我们知道了Flutter的页面是如何构建起来,并进行布局和绘制的,也了解了是通过什么方式节约资源的。希望能对大家有所帮助。

7.参考资料

https://book.flutterchina.club/chapter14/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容