Flutter之Widget层级介绍

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

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

一、Widget介绍

前面说过Widget是一种很是轻量且不可变的数据结构,只起一个组织者做用。那么它是如何轻量的尼?下面咱们就从源码来一窥究竟。markdown

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多几个方法。ide

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

因为RenderObject主要是将视图绘制成不一样的图层,而后再显示在屏幕上。因此只有当咱们的组件直接或间接继承自RenderObjectWidget时,才会经过RenderObject来进行绘制、渲染,从而显示在屏幕上,如RichTextRowCenter等。不然只是一个用来组装组件的容器,如TextListView等。函数

二、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源码里东西仍是蛮多的,但其实只有如下一些核心的方法。post

  • 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的生命周期流程以下:

三、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

四、演示案例

接下来用一个示例来讲明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对象。

五、结束语

固然,Widget东西很是多,不可能仅凭一篇文章就可以描述清楚的,本文也只是从总体结构上来对Widget进行一次描述,方便你们深刻的去了解Widget

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

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

那么你们觉得尼???

【参考资料】

Flutter实战

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

Flutter Dart Framework原理简解

[译]Flutter中的层级结构

Flutter原理与实践

相关文章
相关标签/搜索