前段时间总体看了一下 Flutter 的渲染流程,今天整理成文章分享一下 Flutter 的工作原理。直接从 main 文件里面的 runApp 开始看起:
这里执行了两个方法:
scheduleAttachRootWidgetshceduleWarmUpFrame绑定 root 组件
第一个方法是绑定 root 组件的。这里会创建一个 RenderObjectToWidgetAdapter 对象并执行一个 attachToRenderTree 任务。RenderObejctTOWidgetAdapter这个对象连接 RenderObejct 和 Element 这两个对象。这个对象继承了 RenderObjectWidget ,可以理解是最外层的组件。然后需要把我们实际的 root 组件 attach 上去。它的 container 传入的是 renderView 变量,

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

RenderView 是一个 RenderObejct 对象。prepareInitialFrame :

分别负责第一帧的布局和绘制。这里把自己添加到了需要布局的组件列表。同理,绘制也是加到了需要 paint 的组件列表里去。
attachToRenderTree 则把 renderView 的 renderViewElement,如果 renderView 是 null, 那么就创建:

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

这里根节点就添加上了,然后这里会执行根节点 Element 的 mount 方法:其中会在父类实现里面 createRenderObject 和 attachRenderObject
这里的 createRenderObejct 返回的就是 RenderView ,然后执行 rebuild:
RenderObjectToWidgetElement#rebuild

这里会一直更新 child 节点。
如果 _child 是 null:

这个可以理解成根节点build的情况,也可以理解成是 child 节点被删除了的情况。
如果 _child 不是null:
updateSlotForChild 更新 slot, 如果不一致的话,就更新旧的 childWidget 对象不一样了,则比较类型和 key ,如果一样的话,就比较并更新 slot ,然后把 child 更新成 newWidgetElement 了.然后调用 Element 的 mount ,就算是加到组件树上了。
渲染 Frame的流程
scheduleWarmUpFrame 里面会调用 handleDrawFrame 方法来处理每个 Frame:

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

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

这里由 buildOwner 构建 scope ,然后在调用父类的 drawFrame 实现。
我们先来看下什么是 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 的长度是否变化,如果变化了会重新排序做调整。
那么为什么这么排序呢?这里分析下可以得到原因:
_dirtyElements 来的,如果是深度大的组件排在深度小的组件,那么就很可能会频繁发生子组件 rebuild 之后,继续执行了父组件的 rebuild,这很明显不合理,所以深度小的应该排在深度大的后面。_dirtyElements 里面似乎不会存在,毕竟打脏标记之后都需要 rebuild。了解了 BuildOwner 的作用之后,我们在渲染过程了解之前先过一下 Flutter 复杂的 WidgetsFlutterBinding 对象:
这个对象是 Flutter 框架层的一个很重要的绑定类,它连接了 Flutter framework层和 engine 层。
我们看下他的继承结构:

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

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

布局 -> 合成 -> 绘制 -> 解析语义
这里会按照布局深度从小到大给打上 dirty 标记的 RenderObject 排序。执行它的 _layoutWithoutResize 函数:

这里会执行这个 RenderObject 的布局和语义更新,然后标记为需要绘制(paint)。
这里 performLayout 由每个实现的 RenderObject 来实现。markNeedsSemanticsUpdate 则是标记更新所在的语义树。
performLayout 负责执行布局。是 RenderObejct 的方法。IDE 里面看下子类,基本都是实现了 RenderObjectWidget 的类里面用到了这个。RenderObejctWidget 里面会有对应的 RenderObjectElement :
他的父类常见的有:
LeafRenderObjectWidget 叶子节点, 比如 ErrorWidget 就是继承这个实现的,除了确定一下宽高基本不怎么需要实现 performLayoutMultiChildRenderObjectWidget 多个child节点的,比如 WrapSingleChildRenderObjectWidget 单个child的,比如 SizeBox 用这几个看看: RenderWrap 的 performLayout 就比较复杂:
先根据轴方向来确定大小约束。比如如果是水平方向,就设置一个 Box 约束,最大宽度就是自己本身约束的最大宽度。
这里是累加子元素的宽高。如果一行放不下了,就换行,然后加上垂直轴方向的高度。最终遍历完 child 之后,确定 size :


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

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

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

如果 boundary 不是null,那就会通知父节点去重新布局。你也可以理解成这个就是对应了 Android 的 requestLayout 流程。只是这个流程避免了不必要的重复 layout, 效率更高。
这里也会把需要 compositingBits 的 RenderObject 根据深度从小到大排序。然后执行每个 object 的 _updateCompositingBits 。这样父 node 更新之后子node就可以忽略,避免多次执行。这里会执行 visitChildren ,这个函数的具体实现也由对应的 RenderObject 实现来提供。

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

这个时候如果 isRepaintBoundary 是true并且 needsCompositing 值发生变化的时候,就会执行 markNeedsPaint ,这里会把需要绘制的加入到 _nodesNeedingPaint。如果没有 isRepaintBoundary, 则会一直往上寻找父节点并且打上drity,直到 isRepaintBoundary是false。
这个机制可以让我们在开发中自己合理的指定 RepaintBoundary,这样可以避免不必要的重绘逻辑。
直接看 flushPaint 的逻辑:

这里会处理每个 node 的 layer , 这里的 _layer 是 ContainerLayer . 这个代表的是一个有子列表的合成层。attached 代表这个 node 的根节点是已经附加到组件树上的。这时候会调用 PaintingContext.repaintCompositedChild ,否则就调用 _skippedPaintingOnLayer:
_skippedPaintingOnLayer
这里是为了保证分离的节点重新附加上组件树的时候也会重新渲染。
PaintingContext.repaintCompositedChild
这里是进行 repaint 逻辑的地方。这里会直接调用 _repaintCompositedChild 方法

这里最后调用了 paint 函数:

到这里大致的 Flutter 渲染流程就看完了。这部分工作流程对我们的开发工作还是有一些启发的:
layout 和 paint 中的 Boundary 概念来合理安排我们的布局,避免不必要的 layout 和 paint 逻辑,提升应用的性能。performLayout 等方法的重写的参考,来实现一些特殊需求的自定义 Widget。如果文中我有理解的不对的地方,或者您有不同的理解。y也欢迎评论讨论交流。