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

相关推荐
F-2H34 分钟前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss1 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247553 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255024 小时前
前端常用算法集合
前端·算法
真的很上进4 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203984 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2345 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1235 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~6 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语6 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js