来一份Flutter渲染分析
source link: https://zhuanlan.zhihu.com/p/321300574
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
本文首发于我的公众号 半行代码
定期分享移动端技术和心得体会,欢迎大家关注。
http:// weixin.qq.com/r/xkjA2Cr EN3dIrW679x3U (二维码自动识别)
前段时间总体看了一下 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 的长度是否变化,如果变化了会重新排序做调整。
那么为什么这么排序呢?这里分析下可以得到原因:
_dirtyElements _dirtyElements
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
。
如果文中我有理解的不对的地方,或者您有不同的理解。也欢迎评论讨论交流。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK