6

来一份Flutter渲染分析

 3 years ago
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 这个对象连接 RenderObejctElement 这两个对象。这个对象继承了 RenderObjectWidget ,可以理解是最外层的组件。然后需要把我们实际的 root 组件 attach 上去。它的 container 传入的是 renderView 变量,

Ir63iur.jpg!mobile

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

iq2euyY.jpg!mobile

RenderView 是一个 RenderObejct 对象。 prepareInitialFrame :

QnqAnaj.jpg!mobile

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

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

q2IrEri.jpg!mobile

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

QruIRrJ.jpg!mobile

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

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

RenderObjectToWidgetElement#rebuild

qYbiAfV.jpg!mobile

这里会一直更新 child 节点。

如果 _child 是 null:

R3UVZne.jpg!mobile

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

如果 _child 不是null:

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

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

渲染 Frame的流程

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

iyyeUrv.jpg!mobile

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

v2Az22f.jpg!mobile

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

mmu6fy7.jpg!mobile

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

BuildOwner

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

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

am6zYrJ.jpg!mobile

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

UvYnMb3.jpg!mobile

用一张图表示就是:

jq2eaeY.jpg!mobile

解释一下这段的逻辑:

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 层。

我们看下他的继承结构:

faQZNfj.jpg!mobile

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

drawFrame

继续看会执行 RenderBindingdrawFrame 方法:

AjiuIvb.jpg!mobile

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

bUr6Vzz.png!mobile

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

layout

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

YBRNRj3.jpg!mobile

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

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

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

他的父类常见的有:

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

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

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

QRVnAfj.jpg!mobileVbEJvqy.jpg!mobile

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

layout方法截图

bUNZnur.jpg!mobile

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

6rU7re2.jpg!mobile

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

EvuuAbf.jpg!mobile

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

compositingBits

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

umeAba7.jpg!mobile

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

Z3AzMfI.jpg!mobile

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

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

paint

直接看 flushPaint 的逻辑:

BriU7jy.jpg!mobile

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

_skippedPaintingOnLayer

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

PaintingContext.repaintCompositedChild

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

IBbyu2U.jpg!mobile

这里最后调用了 paint 函数:

uumUfu7.jpg!mobile

总结

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK