【Flutter脱发实录】盘一盘RenderObject

2,427 阅读8分钟

Widget篇中,提到了RenderObject,那么RenderObject到底是个啥?咱们来盘一盘!

官方解释

An object in the render tree.

render树上的一个对象。

Flutter门前有四棵树,一棵是Widget树,一棵是Element树,一棵是Render树,还有一棵是layer树。

如果Widget树是一张图纸,那么Render树就是这张图纸对应的流水线。RenderObject就是这条流水线上的操作员工。

我们在顶层配置好Widget树后,最终render树负责在手机屏幕上表现出你想要的画面。

阅读源码

美好的一天应当从阅读源码开始!

abstract class RenderObject extends AbstractNode 
with DiagnosticableTreeMixin 
implements HitTestTarget {}

可以看到RenderObject也是链表结构,混入了DiagnosticableTreeMixin树状结构的特性,并且实现了命中测试抽象类。

去掉debug和assert的代码,RenderObject的结构功能清晰可见。

Layout 模块

abstract class RenderObject {
  // LAYOUT 模块
  // 传递信息给child的存储容器  通常为偏移量Offset
  ParentData parentData;

  // 此方法,在child加入到child列表之前,把parentData传递给child
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! ParentData)
      child.parentData = ParentData();
  }

  // 添加一个render object 作为自己的child
  @override
  void adoptChild(RenderObject child) {
    // 设置parentData
    setupParentData(child);
    // 标记更新
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
  }

  // 删除一个render object 作为自己的child
  @override
  void dropChild(RenderObject child) {
    // 清除边界
    child._cleanRelayoutBoundary();
    // 失去parentdata访问权限
    child.parentData.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
  }

  // 遍历children
  void visitChildren(RenderObjectVisitor visitor) { }

  // render tree的管理者
  @override
  PipelineOwner get owner => super.owner;

  // 告诉其owner将其插入render tree 并初次标记需要计算layout并且重绘
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if (_needsLayout && _relayoutBoundary != null) {
      _needsLayout = false;
      markNeedsLayout();
    }
    if (_needsCompositingBitsUpdate) {
      _needsCompositingBitsUpdate = false;
      markNeedsCompositingBitsUpdate();
    }
    if (_needsPaint && _layer != null) {
      _needsPaint = false;
      markNeedsPaint();
    }
    if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
      _needsSemanticsUpdate = false;
      markNeedsSemanticsUpdate();
    }
  }

  // 是否需要布局
  bool _needsLayout = true;

  // 记录布局边界在哪
  RenderObject _relayoutBoundary;

  // parent的约束
  @protected
  Constraints get constraints => _constraints;
  Constraints _constraints;

  //标记需要重新layout
  void markNeedsLayout() {
    // 如果自己不是边界,则让parent.markNeedsLayout处理 一直推到边界
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      // owner 把自己加入layout的脏表 请求刷新
      _needsLayout = true;
      if (owner != null) {
        owner._nodesNeedingLayout.add(this);
        owner.requestVisualUpdate();
      }
    }
  }

  // 遍历清除非边界child relayout边界记录  当边界发生变化时,会调用此方法清除边界缓存
  void _cleanRelayoutBoundary() {
    if (_relayoutBoundary != this) {
      _relayoutBoundary = null;
      _needsLayout = true;
      visitChildren((RenderObject child) {
        child._cleanRelayoutBoundary();
      });
    }
  }

  // 仅layout 不重新测量大小
  void _layoutWithoutResize() {
      performLayout();
      markNeedsSemanticsUpdate();
    _needsLayout = false;
    markNeedsPaint();
  }

  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;
    // 确定边界
    // 满足以下四项条件 中的一项  则自己为边界
    // parentUsesSize parent是否关心自己的大小
    // sizedByParent 由parent确认大小
    // constraints.isTight 受严格约束
    // parent 不为 RenderObject 即自己为root节点
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      // 否则使用parent的边界
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    _constraints = constraints;
    // 如果边界发生变化  则遍历清空所有已记录的边界 重新设置
    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
      visitChildren((RenderObject child) {
        child._cleanRelayoutBoundary();
      });
    }
    _relayoutBoundary = relayoutBoundary;
    // sizedByParent 为true 时 才会调用performResize 确定大小
    // 否则在performLayout中确定size
    if (sizedByParent) {
      performResize();
    }
    // 计算layout
      performLayout();
      markNeedsSemanticsUpdate();
    _needsLayout = false;
    markNeedsPaint();
  }

  // size是否只由parent的约束决定  默认为false  永远不会错
  // 当为true时,确定size的工作要在performResize中计算
  @protected
  bool get sizedByParent => false;

  // 子类重写此方法  根据约束 计算自身的size
  // 不能直接调用此方法  而是调用layout方法间接调用  只有sizedByParent为true时才会执行
  @protected
  void performResize();

  // 子类重写此方法
  // 不能直接调用此方法  而是调用layout方法间接调用
  // 作用1:当sizedByParent为false时  根据适应child计算自身size
  // 作用2:遍历调用child.layout 确定child的size和offsets
  @protected
  void performLayout();
}

划一下重点:

  • RenderObject作为Render Tree上的一个对象,但是自己却不能直接改变这棵大树。一举一动都需要传达给PipelineOwner这位管理者去执行。
  • PipelineOwner管理着几个污污的(dirty)小本本,记录了需要变化的节点,根据这些小本本去实际更新Render Tree。当RenderObject需要发生改变时,通知owner把自己写到小本本上(标记为dirty)。可以理解为军训过程中,有什么问题报告给教官,教官统一安排。
  • parent通过setupParentData方法,传递parentData,通常为layout offset
  • performResize方法需要子类重写。计算自身的大小
  • performLayout方法需要子类重写。
  1. 当sizedByParent为false时,计算size。
  2. 2.遍历调用child.layout,确定child的size和offsets。
  • 核心方法是layout
确定`_relayoutBoundary`布局边界
->调用`performResize``performLayout`方法计算大小和位置
->重绘

由于这个流程满足大多数场景,因此当我们真正开发时,只关心重写performResizeperformLayout的实现,而不会去重写layout方法。

Paint 模块

abstract class RenderObject {
  // PAINTING 模块
  RenderObject() {
    // 是否需要混合图层 =  是重绘边界 || 总是混合图层
    _needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
  }

  // 当前是否为重绘边界
  bool get isRepaintBoundary => false;

  // 是否总是新建图层然后合并到原图层
  @protected
  bool get alwaysNeedsCompositing => false;

  // 缓存的layer
  ContainerLayer _layer;

  // 是否_needsCompositing的值需要设置
  bool _needsCompositingBitsUpdate = false;

  // 标记_needsCompositing的值需要重新设置
  void markNeedsCompositingBitsUpdate() {
    if (owner != null)
      owner._nodesNeedingCompositingBitsUpdate.add(this);
  }

  // 是否在一个新的图层绘制然后合并到祖先图层
  // true:在新图层绘制 但是新图层会优先使用缓存图层 以提高性能
  // false:不使用新图层  此时缓存图层一定要置null
  // 当前为repaintBoundary时 _needsCompositing=true 并且会自动给缓存layer赋值为新的OffsetLayer 在此layer上绘制后  合并到祖先图层
  bool _needsCompositing;

  // 遍历设置需要使用图层混合
  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;
  }

  // 是否需要绘制
  bool _needsPaint = true;

  // 标记需要重绘
  void markNeedsPaint() {
    _needsPaint = true;
    // 如果当前为repaintBoundary 则通知owner需要重绘
    if (isRepaintBoundary) {
      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    }
    // 如果当前不为root节点  则让parent判断
    else if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
    }
    // 若当前为root节点  则直接通知owner重绘
    else {
      if (owner != null)
        owner.requestVisualUpdate();
    }
  }

  // 根节点初始化  见[RenderView]
  void scheduleInitialPaint(ContainerLayer rootLayer) {
    _layer = rootLayer;
    owner._nodesNeedingPaint.add(this);
  }

  // 替换rootLayer  只有root节点才会调用
  // 当设备的像素比device pixel ratio变化时  可能会调用此方法
  void replaceRootLayer(OffsetLayer rootLayer) {
    _layer.detach();
    _layer = rootLayer;
    markNeedsPaint();
  }

  // 子类重写此方法 在对应的offset完成真正的绘制操作
  // 不可直接调用此方法  而是用markNeedsPaint标记 让owner去处理
  // 如果只想绘制一个child 则用PaintingContext.paintChild 接口的方式去操作  避免直接操作render object
  void paint(PaintingContext context, Offset offset) { }
}

本模块中最重要的一点就是needsCompositing这个变量。这个变量决定是否在新的layer上绘制。在构造器中可以看到,它由isRepaintBoundaryalwaysNeedsCompositing决定。isRepaintBoundary这个可以通过RepaintBoundary包裹修改其为truealwaysNeedsCompositing可以由子类修改。

因此我们可以通过使用RepaintBoundary这个控件达到局部重绘的目的,以提高性能。

SEMANTICS 语义化模块 和 HIT TESTING 命中测试模块

语义化即Semantics,主要是提供给读屏软件的接口,也是实现辅助功能的基础,通过语义化接口可以让机器理解页面上的内容,对于有视力障碍用户可以使用读屏软件来理解UI内容。除非有特殊的需求,一般接触不到。

通过以下方法,处理命中测试结果。

// 重写此方法处理事件
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }

RenderObject中还有两个比较重要的方法,也提一下。

  // 是否重装 只在debug模式下生效 也就是开发中的hot reload效果
  void reassemble() {
    // 标记为需要重新layout
    markNeedsLayout();
    // 标记重新设置是否使用新图层
    markNeedsCompositingBitsUpdate();
    // 标记需要重绘
    markNeedsPaint();
    // 标记语义化需要更新
    markNeedsSemanticsUpdate();
    // 遍历child的reassemble方法 全部标记一遍
    visitChildren((RenderObject child) {
      child.reassemble();
    });
  }
  
  // 是否显示在屏幕上  viewport中会使用到
  void showOnScreen({
    RenderObject descendant,
    Rect rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    if (parent is RenderObject) {
      final RenderObject renderParent = parent;
      renderParent.showOnScreen(
        descendant: descendant ?? this,
        rect: rect,
        duration: duration,
        curve: curve,
      );
    }
  }

PipelineOwner

The pipeline owner manages the rendering pipeline.

整个渲染流程的管理者,具体表现在如下几个方法:

  1. flushLayout

遍历需要relayout的render object节点,调用_layoutWithoutResize()重写计算布局

  1. flushCompositingBits

遍历需要CompositingBitsUpdate的节点,调用_updateCompositingBits()方法更新needsCompositing。通常在flushLayoutflushPaint之间执行。

  1. flushPaint

遍历需要repaint的节点,通过PaintingContext调用子render object_paintWithContext方法触发paint绘制。

  1. flushSemantics

更新语义化

RenderBox

RenderBoxRenderObject的子类,是在2D笛卡尔坐标系下对RenderObject的进一步封装。它主要封装了如下几个功能点:

  1. parentdata是BoxParentData 只有offset属性 默认为Offset.zero
  2. 使用BoxConstraints作为其约束
  3. 使用size记录其大小
  4. 测量自身最大最小宽高
  5. 测量基线
  6. 实现默认的命中测试方案
  7. 混入RenderObjectWithChildMixin单个child的实现->SingleChildRenderObjectWidget
  8. 混入ContainerRenderObjectMixin多个children的实现->MultiChildRenderObjectWidget

开发App在笛卡尔坐标系上进行绘制,所以继承RenderBox就可以满足大部分场景了。

总结

本节主要记录了RenderObject主要的功能和方法,理解这些内容可以帮助我们更好的理解Flutter UI底层原理,对我们实现自定义的控件也有帮助。至于具体如何根据布局绘制到屏幕上,后文会使用实例分析。

搞懂原理真是伤头发啊!