1. Flutter渲染机制之三棵树
在Flutter中和Widgets一起协同工作的还有另外两个搭档:Elements和RenderObjects;因为它们都是有着树形构造,所以常常会称它们为三棵树。
- Widget:Widget是Flutter的外围局部,是用户界面的不可变形容。做Flutter开发接触最多的就是Widget,能够说Widget撑起了Flutter的半边天;
- Element:Element是实例化的 Widget 对象,通过 Widget 的 createElement() 办法,是在特定地位应用 Widget配置数据生成;
- RenderObject:用于利用界面的布局和绘制,保留了元素的大小,布局等信息;
2. 首次运行时的三棵树
初步意识了三棵树之后,那Flutter是如何创立布局的?以及三棵树之间他们是如何协同的呢?接下来就让咱们通过一个简略的例子来分析下它们外在的协同关系:
class ThreeTree extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.red, child: Container(color: Colors.blue) ); }}
下面这个例子很简略,它由三个Widget组成:ThreeTree、Container、Text。那么当Flutter的runApp()办法被调用时会产生什么呢?
当runApp()被调用时,第一工夫会在后盾产生以下事件:
Flutter会构建蕴含这三个Widget的Widgets树;
Flutter遍历Widget树,而后依据其中的Widget调用createElement()来创立相应的Element对象,最初将这些对象组建成Element树;
接下来会创立第三个树,这个树中蕴含了与Widget对应的Element通过createRenderObject()创立的RenderObject;
下图是Flutter通过这三个步骤后的状态:
从图中能够看出Flutter创立了三个不同的树,一个对应着Widget,一个对应着Element,一个对应着RenderObject。每一个Element中都有着绝对应的Widget和RenderObject的援用。能够说Element是存在于可变Widget树和不可变RenderObject树之间的桥梁。Element善于比拟两个Object,在Flutter外面就是Widget和RenderObject。它的作用是配置好Widget在树中的地位,并且放弃对于绝对应的RenderObject和Widget的援用。
3. 三棵树的作用
简而言之是为了性能,为了复用Element从而缩小频繁创立和销毁RenderObject。因为实例化一个RenderObject的老本是很高的,频繁的实例化和销毁RenderObject对性能的影响比拟大,所以当Widget树扭转的时候,Flutter应用Element树来比拟新的Widget树和原来的Widget树:
//framework.dart @protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { if (newWidget == null) { if (child != null) deactivateChild(child); return null; } Element newChild; if (child != null) { 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); } assert(() { if (child != null) _debugRemoveGlobalKeyReservation(child); final Key key = newWidget?.key; if (key is GlobalKey) { key._debugReserveFor(this, newChild); } return true; }()); return newChild; } static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }
如果某一个地位的Widget和新Widget不统一,才须要从新创立Element;
如果某一个地位的Widget和新Widget统一时(两个widget相等或runtimeType与key相等),则只须要批改RenderObject的配置,不必进行消耗性能的RenderObject的实例化工作了;
因为Widget是十分轻量级的,实例化消耗的性能很少,所以它是形容APP的状态(也就是configuration)的最好工具;
重量级的RenderObject(创立非常消耗性能)则须要尽可能少的创立,并尽可能的复用;
看到这里你是否会感觉整个Flutter APP就像是一个RecycleView呢?
因为在框架中,Element是被抽来到来的,所以你不须要常常和它们打交道。每个Widget的build(BuildContext context)办法中传递的context就是实现了BuildContext接口的Element。
4. 更新时的三棵树
因为Widget是不可变的,当某个Widget的配置扭转的时候,整个Widget树都须要被重建。例如当咱们扭转一个Container的色彩为橙色的时候,框架就会触发一个重建整个Widget树的动作。因为有了Element的存在,Flutter会比拟新的Widget树中的第一个Widget和之前的Widget。接下来比拟Widget树中第二个Widget和之前Widget,以此类推,直到Widget树比拟实现。
class ThreeTree extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.orange, child: Container(color: Colors.blue,), ); }}
Flutter遵循一个最根本的准则:判断新的Widget和老的Widget是否是同一个类型:
如果不是同一个类型,那就把Widget、Element、RenderObject别离从它们的树(包含它们的子树)上移除,而后创立新的对象;
如果是一个类型,那就仅仅批改RenderObject中的配置,而后持续向下遍历;
在咱们的例子中,ThreeTree Widget是和原来一样的类型,它的配置也是和原来的ThreeTreeRender一样的,所以什么都不会产生。下一个节点在Widget树中是Container Widget,它的类型和原来是一样的,然而它的色彩变动了,所以RenderObject的配置也会产生对应的变动,而后它会从新渲染,其余的对象都放弃不变。
留神这三个树,配置产生扭转之后,Element和RenderObject实例没有发生变化。
下面这个过程是十分快的,因为Widget的不变性和轻量级使得他能疾速的创立,这个过程中那些重量级的RenderObject则是放弃不变的,直到与其绝对应类型的Widget从Widget树中被移除。
5. 当Widget的类型产生扭转时的三棵树
class ThreeTree extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.orange, child: FlatButton( onPressed: () {}, child: Text('三棵树'), ), ); }}
和方才流程一样,Flutter会从新Widget树的顶端向下遍历,与原有树中的Widget类型进行比照。
因为FlatButton的类型与Element树中绝对应地位的Element的类型不同,Flutter将会从各自的树上删除这个Element和绝对应的ContainerRender,而后Flutter将会重建与FlatButton绝对应的Element和RenderObject。
当新的RenderObject树被重建后将会计算布局,而后绘制在屏幕下面。Flutter外部应用了很多优化办法和缓存策略来解决,所以你不须要手动来解决这些。