Flutter Framework 渲染流程分析(四):Layer

本文是Flutter Framework 渲染流程分析中的第四篇

上篇我们讲了RenderObjectlayoutpaint流程。layout最终计算出来的布局信息是提供给paint使用的,paintflush阶段会调用到自身的void paint(PaintingContext context, Offset offset)方法,我们来回顾下这个方法。

dart 复制代码
/// code 4.1
///[RenderClipOval] 中的实现
void paint(PaintingContext context, Offset offset) {
    if (child != null) {
        if (clipBehavior != Clip.none) {
            _updateClip();
            layer = context.pushClipPath(
                needsCompositing, 
                offset,
                _clip,
                _getClipPath(_clip!),
                super.paint,
                clipBehavior: clipBehavior,
                oldLayer: layer as ClipPathLayer?,
            );
        } else {
            context.paintChild(child!, offset);
            layer = null;
        }
    } else {
        layer = null;
    }
}

这里引入了两个重要的对象,一个是Layer,一个是PaintingContext_clip 是一个 Rect对象,context.pushClipPath通过_cliplayer的裁剪,得到的新图层重新赋值给layer。先看一下RenderObject中关于layer的定义。

RenderObject 中的 Layer

dart 复制代码
/// code 4.2
/// [RenderObejct] 中的代码
@protected
ContainerLayer? get layer {
  assert(!isRepaintBoundary || _layerHandle.layer == null || _layerHandle.layer is OffsetLayer);
  return _layerHandle.layer;
}

@protected
set layer(ContainerLayer? newLayer) {
  assert(
    !isRepaintBoundary,
    'Attempted to set a layer to a repaint boundary render object.\n'
    'The framework creates and assigns an OffsetLayer to a repaint '
    'boundary automatically.',
  );
  _layerHandle.layer = newLayer;
}

final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>();

layer的操作都会转到_layerHandle上来,LayerHandle的接口比较简单,看下面set layer(T? layer)方法的实现,它只是持有layer并对layer做引用计数而已,也就是这个layer被多少个LayerHandle持有了,当持有数为 0 时,_layer?_unref()里就会对layer做资源释放工作。

dart 复制代码
/// code 4.3
class LayerHandle<T extends Layer> {
  /// Create a new layer handle, optionally referencing a [Layer].
  LayerHandle([this._layer]) {
    if (_layer != null) {
      _layer!._refCount += 1;
    }
  }

  T? _layer;

  /// The [Layer] whose resources this object keeps alive.
  ///
  /// Setting a new value or null will dispose the previously held layer if
  /// there are no other open handles to that layer.
  T? get layer => _layer;

  set layer(T? layer) {
    assert(
      layer?.debugDisposed != true,
      'Attempted to create a handle to an already disposed layer: $layer.',
    );
    if (identical(layer, _layer)) {
      return;
    }
    _layer?._unref();
    _layer = layer;
    if (_layer != null) {
      _layer!._refCount += 1;
    }
  }

  @override
  String toString() => 'LayerHandle(${_layer != null ? _layer.toString() : 'DISPOSED'})';
}

Layer先暂时了解这么多,再看PaintingContext

PaintingContext

dart 复制代码
/// code 4.4
class PaintingContext extends ClipContext {

  /// Creates a painting context.
  ///
  /// Typically only called by [PaintingContext.repaintCompositedChild]
  /// and [pushLayer].
  @protected
  PaintingContext(this._containerLayer, this.estimatedBounds);

  final ContainerLayer _containerLayer;

  /// An estimate of the bounds within which the painting context's [canvas]
  /// will record painting commands. This can be useful for debugging.
  ///
  /// The canvas will allow painting outside these bounds.
  ///
  /// The [estimatedBounds] rectangle is in the [canvas] coordinate system.
  final Rect estimatedBounds;
 
  ...
  // 一系列 layer 操作
  
  ClipRectLayer? pushClipRect(...) ...
  
  ClipRRectLayer? pushClipRRect(...) ...
  
  ClipPathLayer? pushClipPath(...) ...
    
  ColorFilterLayer pushColorFilter(...) ...
      
  TransformLayer? pushTransform(...) ...
        
  OpacityLayer pushOpacity(...) ...

  ...
 }

PaintingContext是绘制上下文,包含一系列 layer 操作,构造函数接受两个参数,一个是_containerLayer,一个是estimatedBoundsestimatedBounds限制了图层的位置和大小,ContainerLayerLayer的一种,从名字就猜到它是用来组合Layer的。根据code 4.1中的代码,看一下pushClipPath的实现

dart 复制代码
/// code 4.5
ClipPathLayer? pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipPathLayer? oldLayer }) {
  if (clipBehavior == Clip.none) {
    painter(this, offset);
    return null;
  }
  final Rect offsetBounds = bounds.shift(offset);
  final Path offsetClipPath = clipPath.shift(offset);
  // 这里我们着重看这条件分支里面的处理
  if (needsCompositing) {
    final ClipPathLayer layer = oldLayer ?? ClipPathLayer();
    layer
      ..clipPath = offsetClipPath
      ..clipBehavior = clipBehavior;
    // 构造一个新的 layer,push 到 _containerLayer 上
    pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
    return layer;
  } else {
    // 通过 canvas 操作将裁剪操作保存到 _currentLayer 上
    clipPathAndPaint(offsetClipPath, clipBehavior, offsetBounds, () => painter(this, offset));
    return null;
  }
}

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

先明确一下,这个方法里面有一个方法传参painter,根据 code 4.1 中的代码,就是super.paint,看它的实现调用[PaintingContext].paintChild的操作,其实就是绘制子节点,无论是pushLayer,还是clipPathAndPaint,都将这个painter操作传进去了。

pushLayer

dart 复制代码
/// code 4.6
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) {
  ...
  appendLayer(childLayer);
  // createContext 实现如下面的代码,创建一个新的绘制上下文,此时的 childLayer 是上面传的 ClipPathLayer
  final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);

  // 执行 child 的绘制
  painter(childContext, offset);
  ...
}

@protected
void appendLayer(Layer layer) {
  ...
  // 移除 layer 原先的 parent(如果存在)
  layer.remove();
  // 加到 _containerLayer 
  _containerLayer.append(layer);
}

@protected
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
  return PaintingContext(childLayer, bounds);

childLayercode 4.5中传进来的ClipPathLayer,已经限制了裁剪区域。pushLayer在把childLayer添加到_containerLayer之后,同时将它传递到childContext去绘制,这样就限制了child绘制和区域,实现裁剪的功能。

clipPathAndRepaint

dart 复制代码
/// code 4.7
void _clipAndPaint(void Function(bool doAntiAlias) canvasClipCall, Clip clipBehavior, Rect bounds, VoidCallback painter) {
  // canvas 操作
  canvas.save();
  switch (clipBehavior) {
    case Clip.none:
      break;
    case Clip.hardEdge:
      canvasClipCall(false);
    case Clip.antiAlias:
      canvasClipCall(true);
    case Clip.antiAliasWithSaveLayer:
      canvasClipCall(true);
      canvas.saveLayer(bounds, Paint());
  }
  // painter 实际上就是 childContext.paint
  painter();
  if (clipBehavior == Clip.antiAliasWithSaveLayer) {
    canvas.restore();
  }
  canvas.restore();
}


void clipPathAndPaint(Path path, Clip clipBehavior, Rect bounds, VoidCallback painter) {
  // 执行 canvas 的裁剪操作
  _clipAndPaint((bool doAntiAlias) => canvas.clipPath(path, doAntiAlias: doAntiAlias), clipBehavior, bounds, painter);
}

根据 clipBehavior,canvas 先做了一个clipPath的裁剪操作,然后再绘制 child。canvas 操作最终是会反映到_currentLayer上的,看下面这段代码:

dart 复制代码
/// code 4.8
bool get _isRecording {
  // 开始记录时,_canvas会被初始化
  final bool hasCanvas = _canvas != null;
  ...
  return hasCanvas;
}

// Recording state
PictureLayer? _currentLayer;
ui.PictureRecorder? _recorder;
Canvas? _canvas;

@override
Canvas get canvas {
  if (_canvas == null) {
    _startRecording();
  }
  assert(_currentLayer != null);
  return _canvas!;
}

void _startRecording() {
  assert(!_isRecording);
  _currentLayer = PictureLayer(estimatedBounds);
  _recorder = ui.PictureRecorder();
  _canvas = Canvas(_recorder!);
  _containerLayer.append(_currentLayer!);
}

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

先看_startRecording方法,开始记录绘制时,_canvas会被初始化,同时会传入_recorder用来记录任务在_canvas上执行的操作,最终在stopRecordingIfNeeded时,通过recorder!.endRecording()将之前记录的绘制动作输出成一个picture,然后赋值给_currentLayer!.picture,注意_currentLayer是在_startRecording时就已经append_containerLayer上的。

无论是pushLayer还是clipPathAndRepaint,其绘制结果都存在于_containerLayer上,同时这个_containerLayer是在 PaintingContext 实例化时由外部提供。还记得我们 code 4.5 中说到的绘制子节点的方法painter,它最终会走到[PaintContext].paintChild这里

dart 复制代码
/// code 4.9
void paintChild(RenderObject child, Offset offset) {
  ...

  if (child.isRepaintBoundary) {
    stopRecordingIfNeeded();
    // 这里会生成一个新的 layer,用来绘制 child
    // _compositeChild 方法也会根据 _needPaint 这个标记去判断是不是要重新生成
    _compositeChild(child, offset);
  } else if (child._wasRepaintBoundary) {
    child._layerHandle.layer = null;
    child._paintWithContext(this, offset);
  } else {
    child._paintWithContext(this, offset);
  }
}

如果是绘制边界,那么新建一个PaintContext去绘制;否则将其绘制到当前的PaintingContext上。

从上面的绘制操作可以看出,每一个节点在绘制时,都要绘制子节点;根据isRepaintBoundary判断要不要生成一个独立的绘制区域,再根据_needsPaint判断是不是要新建一个PaintingContext,这样就能保证每次绘制时只绘制脏区域。

另外,PaintContext里面针对 child layer的处理完成后,最终都会走到 code 4.6 中的appendLayer方法里,这个方法会走到_containerLayer.append,形成Layer Tree

那么顶层layer是哪里来的?这就回归到RenderObject中关于layer的传递了,RenderObject持有着一个LayerHandle,而LayerHandlelayer属性默认是null的。上篇我们讲过RenderObject的继承关系,根RenderObjectRenderView,通过追踪一下RenderView的实例化,就能找到在RenderView._updateMatricesAndCreateNewRootLayer时生成的rootLayer

dart 复制代码
/// code 4.10
void prepareInitialFrame() {
  ...
  scheduleInitialLayout();
  scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
  ...
}

void scheduleInitialPaint(ContainerLayer rootLayer) {
  ...
  // 设置 layer
  _layerHandle.layer = rootLayer;
  ...
  owner!._nodesNeedingPaint.add(this);
}

Matrix4? _rootTransform;

TransformLayer _updateMatricesAndCreateNewRootLayer() {
  _rootTransform = configuration.toMatrix();
  final TransformLayer rootLayer = TransformLayer(transform: _rootTransform);
  rootLayer.attach(this);
  ...
  return rootLayer;
}

root RenderObject 是RenderView,root Layer 是TransformLayerrootLayer.attach到当前RenderView上时,也会将RenderView传递到所有child layer上,也就是所有layerowner都会是同一个RenderView

现在我们能拿到rootLayer了,只是rootLayer怎么去绘制到界面上还不清楚。实际上对于layer这个对象我们目前还一知半解,只知道它里面可能保存着绘制信息。跟前几篇一样,还是从对象的关键属性和继承关系着手看

Layer

dart 复制代码
/// code 4.11

// 1. 如 code 4.3 中提到的,主要是用来配合 LayerHandle 做引用计数和资源释放
int _refCount = 0;

void _unref(){};

// 2. 继承自 AbstractNode 的接口,构成树形结构。从 parent 的返回可以断定每一个节点都是或继承自ContainerLayer
ContainerLayer? get parent => super.parent as ContainerLayer?;

void dropChild(Layer child){};

void adoptChild(Layer child){};

// 3. 兄弟节点双向链接结构,在 append 时会处理这个兄弟节点的关系
Layer? get nextSibling => _nextSibling;

Layer? _nextSibling;

Layer? get previousSibling => _previousSibling;

Layer? _previousSibling;
// 前面提到的节点的关系的形成就在这个方法里面处理
void append(Layer child){};

// 4. 标记这个 Layer 是否已经更新过,标志的节点会在随后走到 addToScene 方法
bool _needsAddToScene = true;

void markNeedsAddToScene() {};

bool get alwaysNeedsAddToScene => false;

// 5. addToScene 是将当前 Layer 中绘制信息通过 builder 推送到 Engine 上绘制
// 生成的绘制信息保存到 _engineLayer 上。
void addToScene(ui.SceneBuilder builder);

vid _addToSceneWithRetainedRendering(ui.SceneBuilder builder){};

// 6. 如 5 中提到的,_engineLayer 是当前 layer 在 Engine 中的映射对象。
ui.EngineLayer? get engineLayer => _engineLayer;

set engineLayer(ui.EngineLayer? value) {};

ui.EngineLayer? _engineLayer;

这段代码比较长,但总的来看 1、2、3 点都是为了形成Layer Tree的;4、5、6 点是跟绘制有关的,_enginelayer 就是这个Layer的绘制结果,它是 Layer 在 Engine 中的映射。在它的子类上,会保存着一些绘制的信息,比如下面这段在ClipRectLayer中的代码

dart 复制代码
// code 4.11
void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
  ...
  if (!_needsAddToScene && _engineLayer != null) {
    // 节点有更新时会走 addRetained,就是会把上次的 _engineLayer 推送到 Engine 中渲染
    builder.addRetained(_engineLayer!);
    return;
  }
  // 否则从新生成 _engineLayer,看以下的实现
  addToScene(builder);
  ...
  _needsAddToScene = false;
}

// Layer 中的 addToScene 是空实现,看子类 ClipRectLayer 的
void addToScene(ui.SceneBuilder builder) {
  ...
  bool enabled = true;
  ...
  if (enabled) {
    engineLayer = builder.pushClipRect(
      clipRect!,
      clipBehavior: clipBehavior,
      oldLayer: _engineLayer as ui.ClipRectEngineLayer?,
    );
  } else {
    engineLayer = null;
  }
  // 绘制子节点,这里是一个递归处理,会更新底下所有的子孙节点的 Layer
  addChildrenToScene(builder);
  if (enabled) {
    builder.pop();
  }
}

对于ClipRectLayer来说,它的绘制信息就是clipRectclipBehavior,然后通过ui.SceneBuilder中的pushClipRect调用Engine中相应的操作方法生成EngineLayer并保存下来。ui.SceneBuilderui.EngineLayer都是Engine对象。

每一个LayeraddToScene都会触发底下所有子孙节点的更新,子孙节点或是命中缓存处理builder.addRetained,或是走重新添加addToScene。不管哪种方式,最终都是利用ui.SceneBuilder去跟Engine层做交互,ui.SceneBulider实例化在RenderView中。

dart 复制代码
// code 4.12
// [RenderView] 中的方法
void compositeFrame() {
  ...
  try {
    final ui.SceneBuilder builder = ui.SceneBuilder();
    final ui.Scene scene = layer!.buildScene(builder);
    if (automaticSystemUiAdjustment) {
      _updateSystemChrome();
    }
    // _view 是 ui.FlutterView,用来绘制界面的而已。render 也对应着 Engine 中的方法。
    _view.render(scene);
    scene.dispose();
    ...
  }
  ...
}

// ContainerLayer 中的 buildScene 方法
ui.Scene buildScene(ui.SceneBuilder builder) {
  updateSubtreeNeedsAddToScene();
  // 这里即是更新 layer tree,builder 会保存有 layer tree 的信息
  addToScene(builder);
  ...
  _needsAddToScene = false;
  // 生成 ui.Scene,返回给 _view 最终渲染到界面上
  final ui.Scene scene = builder.build();
  return scene;
}

ui.SceneBuilder会根据layer tree的信息生成ui.Scene,通过ui.FlutterView最终将ui.Scene渲染到界面上。渲染方法是_view.render,它接受一个ui.Scene对象,而ui.Scene是通过ui.SceneBuilder根据layer tree生成的。

总结

本文讲了跟Paint流程有关的几个重要的概念:Layer :存储着绘制信息的对象,同时持有着 Engine 层的绘制结果 EngineLayer;PaintingContext :绘制上下文,处理 Layer 的操作,构建Layer Tree,同时最后也讲了关于形成的Layer Tree最终是怎么通知到Engine中进行绘制的。

到现在,WidgetElementRenderObjectLayer,包括LayoutPaint流程相关的东西都讲完了,最后一篇文章会根据前面积累的这些知识点,重头梳理一下整个渲染流程,从首帧渲染到我们日常开发中比较关注的部分,做一个完整性补充。

相关推荐
y先森3 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy3 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿4 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡5 小时前
commitlint校验git提交信息
前端
虾球xz6 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇6 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒6 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员6 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐6 小时前
前端图像处理(一)
前端