在flutter
中,一切皆Widget
。无论是显示界面的UI元素,如Text
、Image
、Icon
等;还是功能性组件,如手势检测的GestureDetector
组件、应用主题数据传递的Theme
组件、移除系统组件自带Padding的MediaQuery
组件等。可以说,flutter
界面就是由一个个粒度非常细的Widget
组合起来的。
由于Widget
是不可变的,所以当视图更新时,flutter
会创建新的Widget
来替换旧的Widget
并将旧的Widget
销毁。但这样就会涉及到大量Widget
对象的销毁和重建,从而对垃圾回收造成压力。也因此,flutter
将Widget
设计的十分轻量,并将视图的配置信息与渲染抽象出来,分别交给Element
与RenderObject
。从而使得Widget
只起一个组织者作用,可以将Element
与RenderObject
组合起来,构成一个视图。
前面说过Widget
是一种非常轻量且不可变的数据结构,只起一个组织者作用。那么它是如何轻量的尼?下面我们就从源码来一窥究竟。
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
/// 创建Widget对应的Element对象,Element对象存储了Widget的配置信息
@protected
Element createElement();
/// 判断是否可以更新Widget
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Widget
是一个抽象类,它只有两个方法:
Widget
对应的Element
对象。Widget
是否可更新。根据Widget
的runtimeType
与key
这两个字段来判断。由于Widget
可以将Element
与RenderObject
组合成一个视图,但从上面源码我们可以发现,Widget
并没有创建RenderObject
对象的方法,那么它是如何创建RenderObject
对象的尼?其实是通过RenderObjectWidget
的createRenderObject
方法来创建的,此Widget
是一个非常重要的类,如果不直接或间接继承该类,Widget
就无法显示在界面上。下面我们对RenderObjectWidget
源码一窥究竟。
abstract class RenderObjectWidget extends Widget {
...
const RenderObjectWidget({ Key key }) : super(key: key);
/// RenderObjectWidget对应着RenderObjectElement及其子类
@override
RenderObjectElement createElement();
/// 创建一个RenderObject对象
@protected
RenderObject createRenderObject(BuildContext context);
/// 更新renderObject
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
/// 将renderObject从render树中移除
@protected
void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
RenderObjectWidget
是一个继承自Widget
的子类,但它比Widget
多几个方法。
RenderObject
对象,在该对象中会将视图数据绘制到不同的图层上。笔者认为它对应着Android中ViewManager的addView方法。Widget
所持有的RenderObject
对象。笔者认为它对应着Android中ViewManager的updateViewLayout方法。RenderObject
对象从Render树中移除,也就是销毁RenderObject
对象。笔者认为它对应着Android中ViewManager的removeView方法。由于RenderObject
主要是将视图绘制成不同的图层,然后再显示在屏幕上。所以只有当我们的组件直接或间接继承自RenderObjectWidget
时,才会通过RenderObject
来进行绘制、渲染,从而显示在屏幕上,如RichText
、Row
、Center
等。否则只是一个用来组装组件的容器,如Text
、ListView
等。
Element
是可变的,这里的可变是指Element
拥有自己的生命周期,可以根据生命周期来重用或销毁Element
对象,减少对象的频繁创建及销毁。它承载了视图构建的上下文数据,也是Element
在连接Widget
与RenderObject
的桥梁,Element
与Widget
是一对多的关系。由于Element
是可变的,所以通过Element
将Widget
树的变化(类似React虚拟DOM diff)做了抽象,可以将真正需要修改的部分同步到真实的RenderObject
树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。下面我们来对Element
的源码一窥究竟。
/// Element对象中存储的是widget的配置信息
abstract class Element extends DiagnosticableTree implements BuildContext {
...
/// 是一个非常重要的方法,主要是更新子Element的配置信息。
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {...}
/// 将当前Element添加到Element树中指定的位置,该位置由父级指定
/// 该方法会改变当前Element的状态,由初始化(initial)状态改为活动(active)状态。
@mustCallSuper
void mount(Element parent, dynamic newSlot) {...}
/// 更新当前Element对应的Widget
/// 该方法仅在Element的活动(active)状态期间调用
@mustCallSuper
void update(covariant Widget newWidget) {...}
/// 改变当前Element在父级中的位置
/// 在MultiChildRenderObjectElement或其他具有多个子元素的[RenderObjectElement]子类中调用。如:Flex及其子类(Column、Row)、Stack等
@protected
void updateSlotForChild(Element child, dynamic newSlot) {...}
void _updateSlot(dynamic newSlot) {...}
void _updateDepth(int parentDepth) {...}
/// 从render树中移除当前Element所对应的RenderObject对象
void detachRenderObject() {...}
/// 将RenderObject对象添加到render树中指定的位置上
void attachRenderObject(dynamic newSlot) {...}
...
/// 初始化Widget,创建Widget对应的Element对象。该方法会调用Widget的createElement方法
/// 该方法通常由updateChild方法调用,但也可以由需要对创建Element进行更细粒度控制的子类直接调用
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {...}
...
/// 将指定Element的状态由活动状态改为非活动状态,并从Render树中移除该Element持有的RenderObject对象
@protected
void deactivateChild(Element child) {...}
/// 从Element的子列表中移除指定的子Element,以准备在Element树的其他地方重用子Element
@protected
void forgetChild(Element child);
void _activateWithParent(Element parent, dynamic newSlot) {...}
static void _activateRecursively(Element element) {...}
/// 将Element由初始化状态改为活动状态的具体实现,在mount方法中会调用该方法
@mustCallSuper
void activate() {...}
/// 将Element的状态由活动转变为非活动状态(等待)。处于非活动状态时,该Element不会被Widget使用,亦不会出现在屏幕上。在当前帧结束之前,该Element会一直存在,如果当前帧结束后,该Element尚未被使用,则该Element状态将改变为销毁状态,从而销毁该Element。
@mustCallSuper
void deactivate() {...}
...
/// 将Element的状态由非活动(等待)状态改为销毁状态,销毁当前Element
@mustCallSuper
void unmount() {...}
@override
RenderObject findRenderObject() => renderObject;
//计算Widget的size,size是从RenderObject中获取的的
@override
Size get size {...}
...
/// 当Element的依赖发生变化时会调用该方法
@mustCallSuper /// 这个是必须调用父类方法的注解吧???
void didChangeDependencies() {...}
...
/// 将Element元素标记为dirty并添加到全局列表中,以便下次在下一帧中重新构建
void markNeedsBuild() {...}
/// Called by the [BuildOwner] when [BuildOwner.scheduleBuildFor] has been
/// called to mark this element dirty, by [mount] when the element is first
/// built, and by [update] when the widget has changed.
void rebuild() {...}
/// 在进行一定的检查后调用rebuild()方法
@protected
void performRebuild();
}
Element
源码里东西还是蛮多的,但其实只有以下一些核心的方法。
Element
,它主要有以下几种情况。newWidget == null | newWidget != null | |
---|---|---|
child == null | 返回null | 返回一个新的Element对象 |
child != null | 移除child并返回null | 如果可能就更新child,返回child或者一个新的Element对象。 |
Element
添加到Element
树中指定的位置,该位置由父级指定。该方法会改变当前Element
的状态,由初始化(initial)状态改为活动(active)状态。Element
对应的Widget
。该方法仅在Element
的活动(active)状态期间调用。MultiChildRenderObjectElement
或其他具有多个子元素的RenderObjectElement
子类中调用。如:Flex
及其子类(Column
、Row
)、Stack
等。Element
所对应的RenderObject
对象。RenderObject
对象添加到render树中指定的位置上。Element
对象。该方法会调用Widget
的createElement
方法来创建Element
对象。该方法通常由updateChild方法调用,但也可以由需要对创建Element进行更细粒度控制的子类直接调用。Element
的状态由活动状态改为非活动状态,并从render树中移除该 Element
持有的RenderObject
对象Element
由初始化状态改为活动状态的具体实现,在mount
方法中会调用该方法Element
的状态由活动转变为非活动状态(等待)。处于非活动状态时,该Element
不会被Widget
使用,亦不会出现在屏幕上。在当前帧结束之前,该Element
会一直存在,如果当前帧结束后,该Element
尚未被使用,则该Element
状态将改变为销毁状态,从而销毁该Element
。Element
的状态由非活动(等待)状态改为销毁状态,销毁当前Element
。从上面代码中,我们可以发现Element
在每一帧内都会在以下几种状态之间转换。
createElement
方法创建了一个Elmenet
对象。mount
方法将Elmenet
对象添加到了Elmenet
树中。deactivate
方法将Elmenet
对象从Elmenet
树中移除。unmount
方法将Elmenet
对象销毁。RenderObject
与Widget
、Element
相比,干的活是最苦逼的。因为要进行进行视图的具体渲染,将视图数据绘制成不同层级。如果没有它,视图就无法显示在屏幕上。下面就从源码里一窥究竟。
/// 顾名思义,主要是来绘制界面
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
/// 布局开始
...
@override
void attach(PipelineOwner owner) {...}
/// 将当前RenderObject对象的布局信息标记为dirty
void markNeedsLayout() {...}
/// 将当前RenderObject对象的父对象的布局信息标记为dirty
@protected
void markParentNeedsLayout() {...}
/// 该方法里仅调用了markNeedsLayout与markParentNeedsLayout方法
void markNeedsLayoutForSizedByParentChange() {...}
void _cleanRelayoutBoundary() {...}
void scheduleInitialLayout() {...}
void _layoutWithoutResize() {...}
/// 完成当前渲染对象的布局,也是界面UI真正开始布局的地方。对应着Android中View的layout方法
void layout(Constraints constraints, { bool parentUsesSize = false }) {...}
...
/// 仅使用约束更新渲染对象大小。
/// 仅当[sizesByParent]为true时才调用此函数。
@protected
void performResize();
/// 为当前Render对象计算布局大小,不能直接调用,由layout方法调用
@protected
void performLayout();
@protected
void invokeLayoutCallback(LayoutCallback callback) {...}
/// 旋转当前Render对象(尚未实现)
void rotate({
int oldAngle, // 0..3
int newAngle, // 0..3
Duration time
}) { }
/// 布局结束
// 绘制开始
...
/// 将当前Render对象的合成状态设为dirty
void markNeedsCompositingBitsUpdate() {...}
...
bool _needsPaint = true;
/// 将当前Render对象标记为需要重新绘制
void markNeedsPaint() {...}
void _skippedPaintingOnLayer() {...}
void scheduleInitialPaint(ContainerLayer rootLayer) {...}
/// 图层替换。
void replaceRootLayer(OffsetLayer rootLayer) {...}
void _paintWithContext(PaintingContext context, Offset offset) {...}
...
/// 在该方法中进行真正的绘制,一般由子类重写,
void paint(PaintingContext context, Offset offset) { }
...
/// 绘制结束
...
别看RenderObject
源码那么多。但核心方法其实只有两个。
Widget
指定其在屏幕上的位置。笔者认为它对应着Android中View的layout
方法。draw
方法。通过RenderObject
就可以将一个个Widget
绘制成对应的图层,由于图层往往会非常多,所以直接向GPU传递这些图层数据会非常低效。因此需要在Engine
中将这些图层进行合并及光栅化,最后在将这些处理后的数据传递给GPU。
关于绘制原理的更多知识可以去YouTube看谷歌推出的讲解视频:Flutter’s renderding pipeline
接下来用一个示例来说明Widget
、Element
及RenderObject
三者之间的关系。
_myWidget() {
return Center(
child: Column(
children: [
Text("1111"),
Row(
children: [Text("222"), Text("3333")],
),
Icon(Icons.ac_unit)
],
),
);
}
在上面例子中,有一个Center
来让Widget
居中展示,一个Column
来让Widget
按照垂直方向排列,一个Row
来让Widget
按照水平方向排列,多个Text
及Icon
来展示。
flutter
在对上面的Widget
遍历完成以后,就会创建对应的Widget
树、Element
树及RenderObject
树。如下:
注意:由于
Text
组件不持有RenderObject
对象,所以render树中的Text
只是一个泛指。
可以发现,在上面的三个树中,Widget
、Element
及RenderObject
是一一对应的,在Element
对象中会同时持有Widget
对象及RenderObject
对象。
当然,Widget
东西非常多,不可能仅凭一篇文章就能够描述清楚的,本文也只是从整体结构上来对Widget
进行一次描述,方便大家深入的去了解Widget
。
最后来一个思考题,Flutter
的Widget
粒度为什么那么细?一位阿里大佬给了我一种思路。
由于Flutter借鉴了React Native的差分算法来更新界面。那么粒度越细,更新界面的效果就越好。
那么大家以为尼???
【参考资料】
Flutter实战
高效开发与高性能并存的UI框架——携程Flutter实践
Flutter Dart Framework原理简解
[译]Flutter中的层级结构
Flutter原理与实践