本文不是一篇介绍Flutter入门知识,主要包括Flutter的视图管理和渲染机制。如果在阅读中有任何问题,麻烦Q442953298。欢迎喜欢Flutter的同学交流
Flutter中的树
Widget树
对Flutter有一定了解的同学,可能知道Flutter存在Everything’s a Widget的说法。在我们构建一个FlutterApp的时候,从main函数开始就调起了runApp函数。而runApp传入的参数就是一个Widget。
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page')),
);
}
}
MyApp -> MaterialApp -> MyHomePage -> ...
组成了一个Widget树。
一个Widget的简单结构如下
// DiagnosticableTree 是一个调试类
// Wiget可以说是表现层的基类
abstract class Widget extends DiagnosticableTree {
final Key key;
@protected
Element createElement();
}
而在StatelessWidget 和 State类中都实现了
Widget build(BuildContext context) {}
通过build方法父Widget可以访问到子节点的Widget
几个问题
- Stateless的Build方法由它本身实现,StatefulWidget的Build方法由State类实现。这种设计是出于对性能和数据-状态的响应等多方面的考虑。
- 并不是所有的部件都需要关心数据/状态的变化,比如布局类、文本、图片等。他们对于状态的变化应该是无感知的。所以他们直接调用本身的Build方法来就可以轻松构造表现层。
- StatefulWidget是有状态的Widget,这种说法其实是存在问题的。StatefulWidget继承自Widget,而Widget被@immutable标记,所以StatefulWidget本身也是不可变的。他之所以能够响应状态的变化,完全依赖于State类的实现。
- 在Wiget树更新的时候,必然伴随着部件的创建和销毁.如果将状态交付给其他类管理,则可以在响应状态时轻松的改变Widget树而不影响数据层的维护。这样设计是数据层和表现层分离的良好体现。
- Widget类由@immutable标记,状态不可变。
刚才我们说Widget的可变状态都是由State类进行管理,因此Widget自身的状态其实都是静态的,Widget从构造到被释放,自身的状态都应该保持不变。
Element树
Element - An instantiation of a [Widget] at a particular location in the tree.
一个在树中特定位置的Widget实例。
我们讨论Widget的时候提到Widget实现了一个方法:
@protected
Element createElement();
在StatelessWidget和StatefulWidget重写了这个方法
abstract class StatelessWidget extends Widget {
StatelessElement createElement() => StatelessElement(this);
}
abstract class StatefulWidget extends Widget {
StatefulElement createElement() => StatefulElement(this);
}
StatelessElement构造方法只是简单的将Widget传给父类构造方法
StatelessElement(StatelessWidget widget) : super(widget);
StatefulElement的构造方法则不太相同
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
_state._element = this;
_state._widget = widget;
}
// 1. 调用传入的widget createState 创建了State对象
// 2. 赋值给自己的_state变量(可用get方法访问)
// 3. state对象分别持有了自身 和 传入的widget对象。
StatelessElement 和StatefulElement都继承自ComponentElement。ComponentElement声明了一个抽象方法
Widget build();
StatelessElement和StatefulElement都重写了build方法
// stateless实现
@override
Widget build() => widget.build(this);
// stateful实现
@override
Widget build() => state.build(this);
根据上面的实现我们可以简单的得出一个结论:
Widget的build都是由Element类的build方法触发。
Widget树的构建就和Element树就有了密不可分的关系,他们到底是怎么实现的。
runApp()以后发生了什么
-
runApp(Widget app)
void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() ..attachRootWidget(app) ..scheduleWarmUpFrame(); // WidgetsFlutterBinding.ensureInitialized() 创建了一个WidgetsFlutterBinding对象,他的主要任务就是协调Framework层和Application层的交互、进程、渲染等底层任务 }
-
attachRootWidget(app)
// WidgetsFlutterBinding.dart void attachRootWidget(Widget rootWidget) { _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget ).attachToRenderTree(buildOwner, renderViewElement); } // 首先RenderObjectToWidgetAdapter 创建了 RenderObjectToWidgetAdapter对象 // 接着attachToRenderTree 则生成了renderViewElement // renderViewElement 通过RenderObjectToWidgetAdapter的createElement创建,它继承自RenderObjectToWidgetElement RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this); // RenderObjectToWidgetElement的widget字段持有了RenderObjectToWidgetAdapter对象
WidgetsFlutterBinding 持有了Element树的根节点RenderObjectToWidgetElement;
RenderObjectToWidgetElement持有了Widget树的根节点 RenderObjectToWidgetAdapter
-
关于RenderObjectToWidgetAdapter
RenderObjectToWidgetAdapter({ this.child, this.container, this.debugShortDescription }) : super(key: GlobalObjectKey(container)); // 在第一第二块的attachRootWidget方法里我们将app当做child参数传给了RenderObjectToWidgetAdapter RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget ) final Widget child; // 其中还有个参数需要被注意: container的参数传入的变量是renderView。 renderView 在WidgetsFlutterBinding的构造方法里被创建,他S是后续我们会讲到的RenderObject树的根节点 我们可以通过 WidgetsFlutterBinding.renderView或者WidgetsFlutterBinding.pipelineOwner.rootNode访问到它
-
关于RenderObjectToWidgetElement
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) { if (element == null) { owner.lockState(() { element = createElement(); element.assignOwner(owner); }); owner.buildScope(element, () { element.mount(null, null); }); } else { element._newWidget = this; element.markNeedsBuild(); } return element; } // 在attachToRenderTree方法里会判断element参数是否为null如果为null则调用CreateElement,否则更新已存在的element的widget
-
element.mount(null, null);
上面我们已经提到WidgetsFlutterBinding -> element根节点 RenderObjectToWidgetElement -> widget根节点RenderObjectToWidgetAdapter
element.mount 方法则开启了Element树和Widget树构建的大门
// RenderObjectToWidgetElement mount 挂载 void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); _rebuild(); } // 我们先不关心 super.mount在RenderObjectToWidgetElement的父类做了什么 // _rebuild方法 void _rebuild() { try { _child = updateChild(_child, widget.child, _rootChildSlot); // _child 作为参数传入第一次为null // widget.child // widget是RenderObjectToWidgetAdapter // RenderObjectToWidgetAdapter.child 则是我们最初runApp传入的appWidget } catch (exception, stack) { // 抛出我们在页面上看见的红色背景黄字警告页面 } }
我们终于看到我们最初传入的AppWidget有了用武之地。
-
updateChild方法 是构建和更新Widget树和Element树的灵魂
Element updateChild(Element child, Widget newWidget, dynamic newSlot) { if (newWidget == null) { if (child != null) deactivateChild(child); return null; } if (child != null) { if (child.widget == newWidget) { // 此处为更新slot代码 return child; } if (Widget.canUpdate(child.widget, newWidget)) { // 此处为更新slot代码 child.update(newWidget); return child; } deactivateChild(child); } return inflateWidget(newWidget, newSlot); }
让我们用通俗的语言来描述这段代码的逻辑
1.如果传入的newWidget为空而child不为空,则表明这是一次更新树的操作。newWidget为空表示我们在移除了一个Widget节点和它子Widget树,因此需要我们移除Element对应的child节点2.如果child不为空(表明是更新操作):
2.1如果widget和新传入的newWidget是同一个对象,则说明这个节点没有任何变化返回child即可。(此处表明该节点可能是InheritWidget或者GlobalKey做了标记,此处后面的文章分析)
2.2.如果widget与新传入的newWidget不是同一块内存,但是Widget.canUpdate(new,old) 的 runtimeType以及key都相同,则表明这个widget的深度和类型都没有发生改变,此时child这个Element对象可以继续拿来复用,只需要将旧的widget更新为新的即可。(此处在更新操作里最常见,比如setState后,build方法没有改变child节点的地方,则该节点下的所有widget都会走此处)
2.3如果上面的情况都不满足,比如widget的节点虽然没有变化但是key发生了变化,再比如widget节点的类型由Text转换为Image类型,则这个节点的Element不能再复用,此时需要我们先移除Element节点
3.当child为空,或者widget的key或类型发生了变化,就会走到这里,此时会调用inflateWidget方法,将新widget膨胀为一个Element。
此处用膨胀其实是想说明Element的作用更加底层和重要
-
inflateWidget魔法-一种递归创建Widget树和Element的方式
Element inflateWidget(Widget newWidget, dynamic newSlot) { assert(newWidget != null); final Key key = newWidget.key; if (key is GlobalKey) { final Element newChild = _retakeInactiveElement(key, newWidget); if (newChild != null) { return updatedChild; } } final Element newChild = newWidget.createElement(); newChild.mount(this, newSlot); return newChild; }
Globalkey此处的逻辑先不表,后续系列再主要介绍,当我们重新create了一个newChild的Element后,Element调用了自身的mount方法。
此处有了一个大的设想,StatelessElement和StatefulElement的mout方法将会把他们build方法获得的widget继续updateChild-inflateWidget方式传递下去因此去做了验证 -
ComponentElement
StatelessElement 和 StatefulElement都继承自ComponentElement。在ComponentElement的mount方法里存在void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); _firstBuild(); } // _firstBuild() void _firstBuild() { rebuild(); } // rebuild在element中实现,最后会调用performRebuild //performRebuild 在ComponentElement中实现 void performRebuild() { Widget built; try { built = build(); } catch (e, stack) { //error 处理 } finally { _dirty = false; } try { _child = updateChild(_child, built, slot); assert(_child != null); } catch (e, stack) { //error 处理 } }
最终在performRebuild方法里知道了我们需要的东西。element会调用自身的build实现,获取当前widget节点下的build子节点。这个build方法创建的子节点会在调用updateChild方法的时候传入,因此形成了递归,一层一层的去构建或者说更新我们的element树,和Widget树。也就完整的完成了Element树和Widget树的调用。
SetState以后发生了什么
我们说刚才的流程是一个树创建的流程,那么树的更新是怎么开始的了?
-
State. setState
_element.markNeedsBuild();
-
Element. didChangeDependencies
markNeedsBuild();
这两者都会引发markNeedsBuild()
void markNeedsBuild() {
_dirty = true;
owner.scheduleBuildFor(this);
}
// markNeedsBuild则会标记element被污染
scheduleBuildFor方法会将element add -> owner._dirtyElements里
并且标记element的_inDirtyList 为 true
然后发起一个等待Vsync信号渲染事件,当下一个Vsync信号来临时,会调用DrawFrame方法
此时buildScope方法重新被调用
你可能没有记住这个方法,你可以去上面查找一下attachWidgetTree 没错在第五节。
在attachToRenderTree方法里,如果element是新建的也会调用这个方法,这个方法的目的就是清空owner的_dirtyElements的element。
清空的方式就是调用他们的rebuild方法
_dirtyElements[index].rebuild();
因此回到了上面我们说的流程,更新和构建的区别,只在于构建是从顶点开始,并且是element树与Widget树向下交叉构建,而更新的流程会在Widget节点没有类型和key变化的前提下优先保留element,新建Widget节点的子树。更新的方式也是向下交叉的遍历。
RenderObject树
经过上面的介绍,可能存在很多复杂的逻辑绕来绕去,如果没有耐心查看源代码,大可以简单的认为,element树和Widget树的挂载是向下交叉完成的。
但我们了解Element树的意义似乎并没有表现出来,因为Element没有渲染合成相关的代码。所以可以认为Element并不是Layer合成的帮助者,这时候需要引入一个之前介绍树构建逻辑时忽略的对象RenderObject。
RenderObject的主要实现我们后续系列再表,它的主要作用就是将Widget的布局属性进行layout paint CompositingBits 等方式的计算,最后将渲染的计算结果提交图形引擎。
在Element的实现中
RenderObject get renderObject {
RenderObject result;
void visit(Element element) {
assert(result == null); // this verifies that there's only one child
if (element is RenderObjectElement)
result = element.renderObject;
else
element.visitChildren(visit);
}
visit(this);
return result;
}
如果此对象是RenderObjectElement,则渲染对象是树中此位置处的对象。否则,这个getter将沿着树走下去,直到找到一个RenderObjectElement。
根节点的element 其实继承自RenderObjectElement
RnderObjectElement持有了一个_renderObeject对象,这个对象是由element.widget createRenderObject创建出来的。
在根节点RenderObjectToWidgetAdpater中返回的是RenderObjectWithChildMixin的类,这个类是在RenderObject的Mixin类,他实际返回的是WidgetFlutterBinding中的renderView 也即是pipelineOwner的rootNode。
不是每个Widget都存在CreateRenderObject方法。CreateRenderObject被声明在Widget的子类RenderObjectWidget中。