Flutter之Widget层级介绍

5,587 阅读11分钟

flutter中,一切皆Widget。无论是显示界面的UI元素,如TextImageIcon等;还是功能性组件,如手势检测的GestureDetector组件、应用主题数据传递的Theme组件、移除系统组件自带Padding的MediaQuery组件等。可以说,flutter界面就是由一个个粒度非常细的Widget组合起来的。

由于Widget是不可变的,所以当视图更新时,flutter会创建新的Widget来替换旧的Widget并将旧的Widget销毁。但这样就会涉及到大量Widget对象的销毁和重建,从而对垃圾回收造成压力。也因此,flutterWidget设计的十分轻量,并将视图的配置信息与渲染抽象出来,分别交给ElementRenderObject。从而使得Widget只起一个组织者作用,可以将ElementRenderObject组合起来,构成一个视图。

1、Widget介绍

前面说过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是一个抽象类,它只有两个方法:

  • createElement:该方法是一个抽象方法,需要在子类实现。顾名思义,该方法主要是创建Widget对应的Element对象。
  • canUpdate:该方法主要是判断Widget是否可更新。根据WidgetruntimeTypekey这两个字段来判断。

由于Widget可以将ElementRenderObject组合成一个视图,但从上面源码我们可以发现,Widget并没有创建RenderObject对象的方法,那么它是如何创建RenderObject对象的尼?其实是通过RenderObjectWidgetcreateRenderObject方法来创建的,此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多几个方法。

  • createRenderObject:创建RenderObject对象,在该对象中会将视图数据绘制到不同的图层上。笔者认为它对应着Android中ViewManager的addView方法。
  • updateRenderObject:更新Widget所持有的RenderObject对象。笔者认为它对应着Android中ViewManager的updateViewLayout方法。
  • didUnmountRenderObject:将RenderObject对象从Render树中移除,也就是销毁RenderObject对象。笔者认为它对应着Android中ViewManager的removeView方法。

由于RenderObject主要是将视图绘制成不同的图层,然后再显示在屏幕上。所以只有当我们的组件直接或间接继承自RenderObjectWidget时,才会通过RenderObject来进行绘制、渲染,从而显示在屏幕上,如RichTextRowCenter等。否则只是一个用来组装组件的容器,如TextListView等。

2、Element介绍

Element是可变的,这里的可变是指Element拥有自己的生命周期,可以根据生命周期来重用或销毁Element对象,减少对象的频繁创建及销毁。它承载了视图构建的上下文数据,也是Element在连接WidgetRenderObject的桥梁,ElementWidget是一对多的关系。由于Element是可变的,所以通过ElementWidget树的变化(类似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源码里东西还是蛮多的,但其实只有以下一些核心的方法。

  • updateChild:更新子Element,它主要有以下几种情况。
newWidget == null newWidget != null
child == null 返回null 返回一个新的Element对象
child != null 移除child并返回null 如果可能就更新child,返回child或者一个新的Element对象。
  • mount:将当前Element添加到Element树中指定的位置,该位置由父级指定。该方法会改变当前Element的状态,由初始化(initial)状态改为活动(active)状态。
  • update:更新当前Element对应的Widget。该方法仅在Element的活动(active)状态期间调用。
  • updateSlotForChild:改变当前Element在父级中的位置。在MultiChildRenderObjectElement或其他具有多个子元素的RenderObjectElement子类中调用。如:Flex及其子类(ColumnRow)、Stack等。
  • detachRenderObject:从render树中移除当前Element所对应的RenderObject对象。
  • attachRenderObject:将RenderObject对象添加到render树中指定的位置上。
  • inflateWidget:初始化Widget,创建Widget对应的Element对象。该方法会调用WidgetcreateElement方法来创建Element对象。该方法通常由updateChild方法调用,但也可以由需要对创建Element进行更细粒度控制的子类直接调用。
  • deactivateChild:将子Element的状态由活动状态改为非活动状态,并从render树中移除该 Element持有的RenderObject对象
  • activate:将Element由初始化状态改为活动状态的具体实现,在mount方法中会调用该方法
  • deactivate:将Element的状态由活动转变为非活动状态(等待)。处于非活动状态时,该Element不会被Widget使用,亦不会出现在屏幕上。在当前帧结束之前,该Element会一直存在,如果当前帧结束后,该Element尚未被使用,则该Element状态将改变为销毁状态,从而销毁该Element
  • unmount:将Element的状态由非活动(等待)状态改为销毁状态,销毁当前Element

从上面代码中,我们可以发现Element在每一帧内都会在以下几种状态之间转换。

  • initial:初始化状态,通过createElement方法创建了一个Elmenet对象。
  • active:活动状态,通过mount方法将Elmenet对象添加到了Elmenet树中。
  • inactive:等待状态,通过deactivate方法将Elmenet对象从Elmenet树中移除。
  • defunct:销毁状态,通过unmount方法将Elmenet对象销毁。

Element的生命周期流程如下:

3、RenderObject介绍

RenderObjectWidgetElement相比,干的活是最苦逼的。因为要进行进行视图的具体渲染,将视图数据绘制成不同层级。如果没有它,视图就无法显示在屏幕上。下面就从源码里一窥究竟。

/// 顾名思义,主要是来绘制界面
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<T extends Constraints>(LayoutCallback<T> 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源码那么多。但核心方法其实只有两个。

  • layout:是一个抽象方法,在其子类中具体实现。它主要是实现界面的布局,通过该方法就会给Widget指定其在屏幕上的位置。笔者认为它对应着Android中View的layout方法。
  • paint:是一个抽象方法,在其子类中具体实现。它主要是在界面上进行具体的绘制,界面上多姿多彩的界面就是通过该方法绘制的。笔者认为它对应着Android中View的draw方法。

通过RenderObject就可以将一个个Widget绘制成对应的图层,由于图层往往会非常多,所以直接向GPU传递这些图层数据会非常低效。因此需要在Engine中将这些图层进行合并及光栅化,最后在将这些处理后的数据传递给GPU。

图片来自Flutter原理与实践

关于绘制原理的更多知识可以去YouTube看谷歌推出的讲解视频:Flutter’s renderding pipeline

4、演示案例

接下来用一个示例来说明WidgetElementRenderObject三者之间的关系。

  _myWidget() {
    return Center(
      child: Column(
        children: <Widget>[
          Text("1111"),
          Row(
            children: <Widget>[Text("222"), Text("3333")],
          ),
          Icon(Icons.ac_unit)
        ],
      ),
    );
  }

在上面例子中,有一个Center来让Widget居中展示,一个Column来让Widget按照垂直方向排列,一个Row来让Widget按照水平方向排列,多个TextIcon来展示。

flutter在对上面的Widget遍历完成以后,就会创建对应的Widget树、Element树及RenderObject树。如下:

注意:由于Text组件不持有RenderObject对象,所以render树中的Text只是一个泛指。

可以发现,在上面的三个树中,WidgetElementRenderObject是一一对应的,在Element对象中会同时持有Widget对象及RenderObject对象。

5、结束语

当然,Widget东西非常多,不可能仅凭一篇文章就能够描述清楚的,本文也只是从整体结构上来对Widget进行一次描述,方便大家深入的去了解Widget

最后来一个思考题,FlutterWidget粒度为什么那么细?一位阿里大佬给了我一种思路。

由于Flutter借鉴了React Native的差分算法来更新界面。那么粒度越细,更新界面的效果就越好。

那么大家以为尼???

【参考资料】

Flutter实战

高效开发与高性能并存的UI框架——携程Flutter实践

Flutter Dart Framework原理简解

[译]Flutter中的层级结构

Flutter原理与实践