前段时间总体看了一下 Flutter
的渲染流程,今天整理成文章分享一下 Flutter
的工作原理。直接从 main 文件里面的 runApp
开始看起:
这里执行了两个方法:
scheduleAttachRootWidget
shceduleWarmUpFrame
绑定 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:
- 如果 child 就是新的 widget, 说明节点还在,调用
updateSlotForChild
更新 slot, 如果不一致的话,就更新旧的 child - 如果
Widget
对象不一样了,则比较类型和 key ,如果一样的话,就比较并更新 slot ,然后把 child 更新成newWidget
- 剩下的情况就直接创建新的
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
继续看会执行 RenderBinding
的 drawFrame
方法:
这里就是 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
用这几个看看: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
这里也会把需要 compositingBits
的 RenderObject
根据深度从小到大排序。然后执行每个 object 的 _updateCompositingBits
。这样父 node 更新之后子node就可以忽略,避免多次执行。这里会执行 visitChildren
,这个函数的具体实现也由对应的 RenderObject
实现来提供。
这里如果 node 有多个 child 的时候,就会调用 _updateCompositingBits
:
这个时候如果 isRepaintBoundary
是true并且 needsCompositing
值发生变化的时候,就会执行 markNeedsPaint
,这里会把需要绘制的加入到 _nodesNeedingPaint
。如果没有 isRepaintBoundary
, 则会一直往上寻找父节点并且打上drity,直到 isRepaintBoundary
是false。
这个机制可以让我们在开发中自己合理的指定 RepaintBoundary
,这样可以避免不必要的重绘逻辑。
paint
直接看 flushPaint
的逻辑:
这里会处理每个 node
的 layer
, 这里的 _layer
是 ContainerLayer
. 这个代表的是一个有子列表的合成层。attached
代表这个 node 的根节点是已经附加到组件树上的。这时候会调用 PaintingContext.repaintCompositedChild
,否则就调用 _skippedPaintingOnLayer
:
_skippedPaintingOnLayer
这里是为了保证分离的节点重新附加上组件树的时候也会重新渲染。
PaintingContext.repaintCompositedChild
这里是进行 repaint
逻辑的地方。这里会直接调用 _repaintCompositedChild
方法
这里最后调用了 paint
函数:
总结
到这里大致的 Flutter 渲染流程就看完了。这部分工作流程对我们的开发工作还是有一些启发的:
- 可以利用 Flutter 在渲染的过程中添加的一些回调在debug的时候进行一些布局树的分析、渲染时长的分析等等。
- 可以利用
layout
和paint
中的Boundary
概念来合理安排我们的布局,避免不必要的layout
和paint
逻辑,提升应用的性能。 - 通过对一些组件
performLayout
等方法的重写的参考,来实现一些特殊需求的自定义Widget
。
如果文中我有理解的不对的地方,或者您有不同的理解。y也欢迎评论讨论交流。