来一份Flutter渲染分析

2022-05-10 20:47:48 浏览数 (1)

前段时间总体看了一下 Flutter 的渲染流程,今天整理成文章分享一下 Flutter 的工作原理。直接从 main 文件里面的 runApp 开始看起:

这里执行了两个方法:

  • scheduleAttachRootWidget
  • shceduleWarmUpFrame

绑定 root 组件

第一个方法是绑定 root 组件的。这里会创建一个 RenderObjectToWidgetAdapter 对象并执行一个 attachToRenderTree 任务。RenderObejctTOWidgetAdapter这个对象连接 RenderObejctElement 这两个对象。这个对象继承了 RenderObjectWidget ,可以理解是最外层的组件。然后需要把我们实际的 root 组件 attach 上去。它的 container 传入的是 renderView 变量,

这是 RenderBinding 的变量,在初始化的时候进行了初始化:

RenderView 是一个 RenderObejct 对象。prepareInitialFrame :

分别负责第一帧的布局和绘制。这里把自己添加到了需要布局的组件列表。同理,绘制也是加到了需要 paint 的组件列表里去。

attachToRenderTree 则把 renderViewrenderViewElement,如果 renderViewnull, 那么就创建:

这里会创建一个 RenderObjectToWidgetElement ,然后确定需要更新的范围。如果这个 Element 是已经存在的,就标记为需要 build

这里根节点就添加上了,然后这里会执行根节点 Elementmount 方法:其中会在父类实现里面 createRenderObjectattachRenderObject

这里的 createRenderObejct 返回的就是 RenderView ,然后执行 rebuild:

RenderObjectToWidgetElement#rebuild

这里会一直更新 child 节点。

如果 _child 是 null:

这个可以理解成根节点build的情况,也可以理解成是 child 节点被删除了的情况。

如果 _child 不是null:

  1. 如果 child 就是新的 widget, 说明节点还在,调用 updateSlotForChild 更新 slot, 如果不一致的话,就更新旧的 child
  2. 如果 Widget 对象不一样了,则比较类型和 key ,如果一样的话,就比较并更新 slot ,然后把 child 更新成 newWidget
  3. 剩下的情况就直接创建新的 Element 了.

然后调用 Element 的 mount ,就算是加到组件树上了。

渲染 Frame的流程

scheduleWarmUpFrame 里面会调用 handleDrawFrame 方法来处理每个 Frame:

这里只负责执行回调,回调则是在 RenderBinding 初始化的时候添加的:

这里面就是关键的逻辑了:

这里由 buildOwner 构建 scope ,然后在调用父类的 drawFrame 实现。

BuildOwner

我们先来看下什么是 BuildOwner,这是一个framework层的管理类。这个类会判断哪些 Widget 需要 rebuild,同时处理 widget 树的其他任务。比如维护处于 inactive 状态的组件的列表。总而言之,就是协助 Flutter 去维护组件树的一个对象。

buildScope 则是完成这个工作的具体实现,来确定组件树更新的范围。然后按照组件深度的顺序来构建有 drity 标记的元素。其中有一个 debugPrintBuildScope 参数可以debug 的时候打印信息,这样组件树更新的时候有日志。

这里就是排序遍历 _dirtyElements 然后执行 rebuild 。我们看下排序规则:

用一张图表示就是:

解释一下这段的逻辑:

a和b 两个 Element , a的节点深度小于b,那么 a 排在 b 后面。

如果 b的深度小于 a, 那么 a 排在 b 的前面。

如果 b 需要重建,a不需要,那么 a 排在 b 后面。

如果 a 需要重建, b 不需要,那么 b 排在 a 后面。

也就是:需要重建的节点排在不需要重建的节点前面,深度小的节点排在深度大的节点后面。

当然,在这个函数执行的时候也有可能会发生其他的setState,所以这里每处理完一个 Element 都会去检查一下 _dirtyElements 的长度是否变化,如果变化了会重新排序做调整。

那么为什么这么排序呢?这里分析下可以得到原因:

  • 深度小的排在深度大的前面:组件rebuild的逻辑正常是按照 _dirtyElements 来的,如果是深度大的组件排在深度小的组件,那么就很可能会频繁发生子组件 rebuild 之后,继续执行了父组件的 rebuild,这很明显不合理,所以深度小的应该排在深度大的后面。
  • 需要重建的排在不需要重建的节点前面,保证 rebuild 的执行顺序不会错,但是这种情况其实 _dirtyElements 里面似乎不会存在,毕竟打脏标记之后都需要 rebuild。

WidgetsFlutterBinding

了解了 BuildOwner 的作用之后,我们在渲染过程了解之前先过一下 Flutter 复杂的 WidgetsFlutterBinding 对象:

这个对象是 Flutter 框架层的一个很重要的绑定类,它连接了 Flutter framework层和 engine 层。

我们看下他的继承结构:

它除了继承自己的 BindingBase 对象,还混入了非常多的 Binding 对象。分别处理不同层的逻辑,职责区分的非常清楚。分别对应了:手势、队列调度、服务、渲染、组件、语义树、绘图。

drawFrame

继续看会执行 RenderBindingdrawFrame 方法:

这里就是 Flutter 绘制的核心流程:

布局 -> 合成 -> 绘制 -> 解析语义

layout

这里会按照布局深度从小到大给打上 dirty 标记的 RenderObject 排序。执行它的 _layoutWithoutResize 函数:

这里会执行这个 RenderObject 的布局和语义更新,然后标记为需要绘制(paint)。

这里 performLayout 由每个实现的 RenderObject 来实现。markNeedsSemanticsUpdate 则是标记更新所在的语义树。

performLayout 负责执行布局。是 RenderObejct 的方法。IDE 里面看下子类,基本都是实现了 RenderObjectWidget 的类里面用到了这个。RenderObejctWidget 里面会有对应的 RenderObjectElement :

他的父类常见的有:

  • LeafRenderObjectWidget 叶子节点, 比如 ErrorWidget 就是继承这个实现的,除了确定一下宽高基本不怎么需要实现 performLayout
  • MultiChildRenderObjectWidget 多个child节点的,比如 Wrap
  • SingleChildRenderObjectWidget 单个child的,比如 SizeBox 用这几个看看: RenderWrapperformLayout 就比较复杂:

先根据轴方向来确定大小约束。比如如果是水平方向,就设置一个 Box 约束,最大宽度就是自己本身约束的最大宽度。

这里是累加子元素的宽高。如果一行放不下了,就换行,然后加上垂直轴方向的高度。最终遍历完 child 之后,确定 size :

接下来还会根据 runAlignment 来调整间距的大小等等。这里不再细究。总之能确认 performLayout 就是类似 Android 的 measure layout , 来确定 UI 组件的大小和位置。这里还能看到 Wrap 的大小其实是根据 child 的大小来计算的, child 的大小是调用了 RenderObejctlayout 得到的。

layout方法截图

其实最后也是调了 performLayout ,但是在调用前处理了一下 boundary , 这其实也是一个 RenderObject 对象:

如果 parentUsesSize 是false的话,那说明布局后不会影响父布局,那么 boundary 就是自己。否则就是父节点的 boundary . boudary的具体用处则在处理 drity 节点的时候。在 RenderObjectmarkNeedsLayout 的时候会进行判断:

如果 boundary 不是null,那就会通知父节点去重新布局。你也可以理解成这个就是对应了 Android 的 requestLayout 流程。只是这个流程避免了不必要的重复 layout, 效率更高。

compositingBits

这里也会把需要 compositingBitsRenderObject 根据深度从小到大排序。然后执行每个 object 的 _updateCompositingBits 。这样父 node 更新之后子node就可以忽略,避免多次执行。这里会执行 visitChildren ,这个函数的具体实现也由对应的 RenderObject 实现来提供。

这里如果 node 有多个 child 的时候,就会调用 _updateCompositingBits :

这个时候如果 isRepaintBoundary 是true并且 needsCompositing 值发生变化的时候,就会执行 markNeedsPaint ,这里会把需要绘制的加入到 _nodesNeedingPaint。如果没有 isRepaintBoundary, 则会一直往上寻找父节点并且打上drity,直到 isRepaintBoundary是false。

这个机制可以让我们在开发中自己合理的指定 RepaintBoundary,这样可以避免不必要的重绘逻辑。

paint

直接看 flushPaint 的逻辑:

这里会处理每个 nodelayer , 这里的 _layerContainerLayer . 这个代表的是一个有子列表的合成层。attached 代表这个 node 的根节点是已经附加到组件树上的。这时候会调用 PaintingContext.repaintCompositedChild ,否则就调用 _skippedPaintingOnLayer:

_skippedPaintingOnLayer

这里是为了保证分离的节点重新附加上组件树的时候也会重新渲染。

PaintingContext.repaintCompositedChild

这里是进行 repaint 逻辑的地方。这里会直接调用 _repaintCompositedChild 方法

这里最后调用了 paint 函数:

总结

到这里大致的 Flutter 渲染流程就看完了。这部分工作流程对我们的开发工作还是有一些启发的:

  • 可以利用 Flutter 在渲染的过程中添加的一些回调在debug的时候进行一些布局树的分析、渲染时长的分析等等。
  • 可以利用 layoutpaint 中的 Boundary 概念来合理安排我们的布局,避免不必要的 layoutpaint 逻辑,提升应用的性能。
  • 通过对一些组件 performLayout 等方法的重写的参考,来实现一些特殊需求的自定义 Widget

如果文中我有理解的不对的地方,或者您有不同的理解。y也欢迎评论讨论交流。

0 人点赞