Widget, Element, RanderObject 三者之间的关系
Widget纯作为一个配置文件存在,可以理解为一个数据结构。Element作为配置文件的实例化对象,具有生命周期的概念,承载构建上下文数据,且持有RenderObject,系统通过遍历Element来构建RenderObject数据,具体Layout,Paint交给RenderObject来完成。Element更像是一个中间层,隔开了Widget与RenderObject,同时也在正常开发过程中将开发者与RenderObject隔开,开发者大部分情况下只需要关注Widget即可。
Widget
Widget类和Element类一一对应。Element是通过Widget生成的
Widget是Element的配置数据,保存UI相关参数。Widget十分轻量,可以频繁的销毁和重建。
Widget.createElement()
创建Element对象
Widget.canUpdate(..)1
2
3
4static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
主要用于在Widget树重新build时复用旧的widget,其实具体来说,应该是:是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置;
通过其源码我们可以看到,只要 newWidget 与 oldWidget 的 runtimeType和key 同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element。
State
一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存UI状态信息.
createState()
用于创建和StatefulWidget相关的状态,它在StatefulWidget的生命周期中可能会被多次调用。例如,当一个StatefulWidget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例。createState()会在Element实例化的时候调用,所以,本质上就是一个StatefulElement对应一个State实例。
widget和context
widget, 它表示与该State实例关联的widget实例,由Flutter framework动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,Flutter framework会动态设置State.widget为新的widget实例。
context, StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext。Elements继承自BuildContext
State如何被Widget复用
只有在Element实例化时会创建新的State。当Widget重新build,Element的updateChild会调用canUpdate(…)方法,若返回true,将child element的widget更新;若返回false,则会重新create新的element。
Element
最终的UI树其实是由一个个独立的Element节点构成。组件最终的Layout、渲染都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。
下面从Element的 挂载、更新、卸载过程来描述Element
mount
根Element的mount
1 | - main() |
以上是根Element的mount过程,其中rederViewElement是根Elemet
** Element的mount过程
首先来看一下常用的StatelessElement,StatefulElement的继承关系
1 | Element -> ComponentElement -> StatelessElement |
1 | - mount() |
Element.updateChild方法非常重要
1 |
|
根据上面的代码,总结各种case
- child != null && newWidget == null :
newWidget为空,说明 Widget.build返回了null, child element对应的widget已经不在widget树上,所以也要移除该element - child == null && newWidget != null :
第一次挂载child Element,解析build出的widget,调用 inflateWidget(newWidget, newSlot) - child != null && newWidget != null :
更新Element,StatefulWidget 调用setState时会进入这个分支。 会调用 child.canUpdate这个方法,判断是否需要更新Element。如果canUpdate返回false,移除这个element然后重新创建 - child == null && newWidget == null :
do nothing
在mount过程会进入第二个分支,调用 inflateWidget(newWidget, newSlot)
1 |
|
1 | abstract class StatefulWidget extends Widget { |
StatefulWidget,State,StatefulElement的调用关系:
Widget.createElement() -> Element.build() -> State.build()
更新Element
1 | Widget.setState |
当调用statState之后,会将element标脏,并加入到_dirtyElements列表中
1 | - WidgetsBinding.drawFrame() |
每次UI刷新时,会回调WidgetBinding的drawFrame(), BuildOwner会将_dirtyElements列表rebuild。
可以翻上去查看挂载阶段rebuild之后的调用链,是完全一样的
卸载Element
在Element的updateChild方法中,在 child != null && newWidget == null
时,会调用deactivateChild()
1 |
|
deactivateChild()方法中,会将child element加入到 BuildOwner的_inactiveElements集合中。
还是WidgetBind.drawFrame()方法,会调用 buildOwner.finalizeTree(); 对不再使用的Element进行unmount
1 | void finalizeTree() { |
RenderObject
RenderObjectElement
RenderObjectElement使用RenderObjectWidget作为配置文件。在RenderTree中有一个与之对应的RenderObject,用来执行具体的测量,绘制等操作。不是所有的Element都有对应的RenderObject
RenderObjectElement有三个常用的子类:
- LeafRenderObjectElement:Leaf render objects, with no children
- SingleChildRenderObjectElement:A single child
- MultiChildRenderObjectElement:A linked list of children.
RenderObjectElement的mount、update、unmount逻辑与ComponentElement大致相同,只不过加上了RenderObject的相关逻辑。
mmount、update、unmount
1 |
|
RenderObject的更新
我们从render树的insert过程类分析RenderObject的更新
从RenderObjectElement.insertChildRenderObject开始
1 |
|
这里调用了ContainerRenderObjectMixin的insert方法,
1 | void insert(ChildType child, { ChildType after }) { |
和element的build流程不同,RenderObject的标脏会向上标脏。
找到 _relayoutBoundary 节点,触发owner._nodesNeedingLayout.add( this )
与owner.requestVisualUpdate()
其中owner._nodesNeedingLayout.add( this )将当前RenderObject注册进PipelineOwner的待刷新列表中,然后触发“VisualUpdate”。这里的owner是PipelineOwner
PiplelineOwner.requestVisualUpdate会请求engine进行一次刷新。。
(todo 不是标脏吗? 难道不是等待vsync信号对标脏的node进行重绘???)
来看一下PiplelineOwner.requestVisualUpdate
1 | void requestVisualUpdate() { |
onNeedVisualUpdate是在PiplelineOwner实例化的时候赋值的,在RendererBinding的initInstances里
1 | void initInstances() { |
ensureVisualUpdate 会调用window.scheduleFrame()请求native层绘制新帧。。。没太搞懂为什么要主动请求绘制
layout
RendererBinding.drawFrame
1 |
|
pipelineOwner.flushLayout()1
2
3
4
5
6
7
8
9
10
11
12
13
14void flushLayout() {
try {
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
} finally {
...
}
}
*RendererObject._layoutWithoutResize
1 | void _layoutWithoutResize() { |
这里调用performLayout()进行layout,并标记需要paint
paint
之前说到过,layout过程中会对paint进行标脏
markNeedsPaint()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) {
if (owner != null) {
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent;
parent.markNeedsPaint();
} else {
// If we're the root of the render tree (probably a RenderView),
// then we have to paint ourselves, since nobody else can paint
// us. We don't add ourselves to _nodesNeedingPaint in this
// case, because the root is always told to paint regardless.
if (owner != null)
owner.requestVisualUpdate();
}
}
和layout的标脏过程类似,一直向上标脏,直到 isRepaintBoundary 为true。注意,这里只有 RepaintBoundary 才加入到了 PiplineOwner 的_nodesNeedingPaint中,即最终是以 RepaintBoundary 单位进行刷新的
需要说明的是,我们可以 RepaintBoundary 是一个widget,我们可以手动添加,来终止paint的向上标脏过程,来达到局部重绘,提升性能。 LayoutBoundary 则是只是一个标记位,framework层帮我们判断的,我们不需要手动处理。
pipelineOwner.flushLayout()
1 | void flushPaint() { |
与layout不同的是这里的 sort 规则是相反的,最深的节点先重绘。并且,多了判断 node._layer.attached
, 如果为true,重绘,并且重绘逻辑交给了PaintContext;如果为false,则执行 node._skippedPaintingOnLayer();
node._skippedPaintingOnLayer()
1 | void _skippedPaintingOnLayer() { |
我的理解是,该节点的 layer 被 detached 了,需要对该节点及其父节点行重新标脏,保证该节点的 layer 被重新 attach 之后能够重绘。
PaintingContext.repaintCompositedChild(node);
1 | static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) { |
又是 PaintContext 和 layer。。。先跳过这两个,最终重绘的逻辑在 child._paintWithContext(childContext, Offset.zero);
中
child._paintWithContext(childContext, Offset.zero);
1 | void _paintWithContext(PaintingContext context, Offset offset) { |