三棵树的关系
上篇文章我们从Flutter的main.dart入手, 一路探讨了三棵树Widget、Element、RenderObject的创建流程和三棵树的关系。简单回顾下三棵树的创建流程和关系:
- Widget树的根节点是RenderObjectToWidgetAdapter(继承自 RenderObjectWidget extends Widget), 我们runApp中传递的Widget树就被追加到了这个树根的child属性上。
- Element树的根节点是RenderObjectToWidgetElement(继承自RootRenderObjectElement extends RenderObjectElement extends Element)。通过调用
RenderObjectToWidgetAdapter的createElement方法创建。创建RenderObjectToWidgetElement的时候把RenderObjectToWidgetAdapter(创建方法:RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);)通过构造参数传递进去,所以 Element 的 _widget 属性值为 RenderObjectToWidgetAdapter 实例,也就是说 Element 树中 _widget 属性持有了 Widget 树实例。RenderObjectToWidgetAdapter 。 - RenderObject 树的根结点是 RenderView(
RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>),在 Element 进行 mount 时通过调用 Widget 树(RenderObjectToWidgetAdapter)的createRenderObject方法获取 RenderObjectToWidgetAdapter 构造实例化时传入的 RenderView 渲染树根节点。
简答的回顾, 我们可以知道如下几点:
- Widget树: Widget是不可变的。开发者使用声明式写出来的UI界面, Widget仅仅只持有控件的配置信息, 是一个配置, 并不会参与UI渲染。所以即使Widget会频繁的创建和销毁, 也不会影响到渲染的性能。
- Element树: Element是不稳定的。Element是树中特定位置Widget的一个实例化对象。Element 是通过遍历 Widget树时,调用 Widget 的方法创建的。Element同时持有Widget和RenderObject。Element 承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。
- RenderObject树: 从名字可以直观知道, RenderObject是主要实现视图渲染的对象。RenderObject渲染视图分为四个阶段, 即布局、绘制、合成、渲染。其中, 布局、绘制在RenderObejct中完成, Flutter采用深度优先遍历渲染树对象, 确定树中各个对象的位置和尺寸, 并把它们绘制在不同的图层上。绘制完毕后, 合成、渲染的工作则交给Skia完成。
三棵树的关系图如下:
上面我们知道了三棵树是什么?
接下来我们一起学习下三棵树的作用。
三棵树的作用
三棵树是为了性能。Widget只是提供UI展示需要的配置信息, 所以Widget是非常轻量级的, 实例化耗费的性能很少。RenderObject是重量级的, 频繁的实例化和销毁RenderObject对性能的影响比较大, 所以Flutter需要复用Element从而减少频繁创建和销毁RenderObject。所有当Widget树的配置信息改变的时候, Flutter使用Element树来比较新的Widget树和原来的Widget树。那么, Element是怎样比较、根据什么来判断是更新Widget还是新建Widget呢?
abstract class Element extends DiagnosticableTree implements BuildContext {
/// Creates an element that uses the given widget as its configuration.
///
/// Typically called by an override of [Widget.createElement].
Element(Widget widget)
: assert(widget != null),
_widget = widget;
......
RenderObject? get renderObject {
RenderObject? result;
void visit(Element element) {
assert(result == null); // this verifies that there's only one child
if (element._lifecycleState == _ElementLifecycle.defunct) {
return;
} else if (element is RenderObjectElement) {
result = element.renderObject;
} else {
element.visitChildren(visit);
}
}
visit(this);
return result;
}
@protected
@pragma('vm:prefer-inline')
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
final Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
// When the type of a widget is changed between Stateful and Stateless via
// hot reload, the element tree will end up in a partially invalid state.
// That is, if the widget was a StatefulWidget and is now a StatelessWidget,
// then the element tree currently contains a StatefulElement that is incorrectly
// referencing a StatelessWidget (and likewise with StatelessElement).
//
// To avoid crashing due to type errors, we need to gently guide the invalid
// element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition
// returns false which prevents us from trying to update the existing element
// incorrectly.
//
// For the case where the widget becomes Stateful, we also need to avoid
// accessing `StatelessElement.widget` as the cast on the getter will
// cause a type error to be thrown. Here we avoid that by short-circuiting
// the `Widget.canUpdate` check once `hasSameSuperclass` is false.
assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
/// 更新
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner!._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
} else {
deactivateChild(child);
assert(child._parent == null);
///
newChild = inflateWidget(newWidget, newSlot);
}
} else {
/// 惊天秘密!!! 首次加载,
newChild = inflateWidget(newWidget, newSlot);
}
......
return newChild;
}
@mustCallSuper
void update(covariant Widget newWidget) {
// This code is hot when hot reloading, so we try to
// only call _AssertionError._evaluateAssertion once.
assert(
_lifecycleState == _ElementLifecycle.active
&& widget != null
&& newWidget != null
&& newWidget != widget
&& depth != null
&& Widget.canUpdate(widget, newWidget),
);
_widget = newWidget;
}
void updateSlotForChild(Element child, Object? newSlot) {
......
/// 只会更新对应的element树, 并将对应的render树进行更新
void visit(Element element) {
element._updateSlot(newSlot);
if (element is! RenderObjectElement)
element.visitChildren(visit);
}
visit(child);
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
由以上源码可知以下几点:
- Element调用canUpdate方法, 通过对比Widget树的类型、key, 来判断是否可更新树、还是重新创建树
- 某一个位置的Widget和新的Widget不一致, 那么, 需要重新创建Element、RenderObject, 频繁创建, 耗费性能
- 某一个位置的Widget和新的Widget一致时, 则只需要修改RenderObject的配置, 不用进行耗费性能的RenderObject的实例化工作了
因此, 可以看出, 即使外面的 widget 树经常变换重建,我们的 Element 可以维持相对稳定,不会重复创建,当然也就不会重复 mount, 生成 RenderObject,只需要以最小代价更新相关属性即可,最大可能减小了性能消耗。Widget 本身只是一些配置信息,简单的对象,它的变更重建不直接影响渲染,对性能影响很小。
总结
上面分析了Widget、Element、RenderObject的联系。下面简单来个总结。
Flutter遍历Widget树时, 调用Widget里面的createElement方法生成对应节点的Element对象, 同时Element对象持有该Widget对象。
特别地, StatefulElement对象创建的时候, 也执行StatefulWidget里面的createState方法创建state, 并且当前Widget也赋值给了state里的_widget, 然后state赋值给了Element里的_state属性。
StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。Widget build() => state.build(this);
问题
createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?
答:Flutter 会在遍历 Widget 树时调用 Widget 里面的 createElement 方法去生成对应节点的 Element 对象,同时执行 StatefulWidget 里面的 createState 方法创建 state,并且赋值给 Element 里的 _state 属性,当前 widget 也同时赋值给了 state 里的_widget,state 里面有个 widget 的get 方法可以获取到 _widget 对象。
build 方法是在什么时候调用的?
答:Element 创建好以后 Flutter 框架会执行 mount 方法,对于非渲染的 ComponentElement 来说 mount 主要执行 widget 里的 build 方法,StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是常见的 BuildContext
BuildContext 是什么?
答:StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。简而言之 BuidContext 就是 Element。
Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?
答:不会影响性能,widget 只是简单的配置信息,并不直接涉及布局渲染相关。Element 层通过判断新旧 widget 的runtimeType 和 key 是否相同决定是否可以直接更新之前的配置信息,也就是替换之前的 widget,而不必每次都重新创建新的 Element。
创建 Widget 里面的 Key 到底是什么作用?
答:Key 作为 Widget 的标志,在widget 变更的时候通过判断 Element 里面之前的 widget 的 runtimeType 和 key来决定是否能够直接更新。