美文网首页
Flutter Framework 渲染流程分析(四):Laye

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

作者: BlueSocks | 来源:发表于2023-08-08 11:13 被阅读0次

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

    /// 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

    /// 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做资源释放工作。

    /// 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

    /// 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的实现

    /// 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

    /// 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

    /// 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上的,看下面这段代码:

    /// 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这里

    /// 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

    /// 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

    /// 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中的代码

    // 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中。

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

    相关文章

      网友评论

          本文标题:Flutter Framework 渲染流程分析(四):Laye

          本文链接:https://www.haomeiwen.com/subject/wupnpdtx.html