Flutter中Element的复用

2,346 阅读10分钟

Flutter简介

Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高性能。开发者可以通过 Dart 语言开发 App,一套代码同时运行在 iOS 和 Android平台。 另外Flutter 提供了丰富的组件、接口,开发者可以很快地为 Flutter 添加 Native 的扩展(以插件的形式嵌入)。

Flutter的本质是一套UI系统,此文主要讲解Flutter中的UI组件的复用。

Widget、Element、RenderObject

Flutter中 everything is widget, widget 的定义是“描述一个UI元素的配置信息”,Widget 并不是表示最终绘制在设备屏幕上的显示元素,最终的UI树其实是由一个个独立的Element节点构成,而最终的Layout、渲染是通过RenderObject来完成的。UI从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。

Widget Tree、Element Tree 和 RenderObject Tree的作用如下:

  • Widget: Element 的配置信息,注意Widget是不可变的。与Element的关系可以是一对多,一份配置可以创造多个Element实例。
  • Element:Widget 的实例化,内部持有Widget和RenderObject。
  • RenderObject:负责布局、渲染绘制。

三者的依赖关系是:Element树根据Widget树生成,而渲染树又依赖于Element树

Element的生命周期

    1. Flutter Framework 调用Widget.createElement 创建一个Element实例,记为element
    1. Flutter Framework 调用 element.mount(parentElement,newSlot)mount方法中首先调用elment所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置. 插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。

    1. 当element的Widget配置数据改变时,为了进行Element复用,Framework在决定重新创建Element前会先尝试复用相同位置旧的element,调用Element对应Widget的canUpdate()方法,如果返回true,则复用旧Element,旧的Element会使用新的Widget配置数据更新,反之则会创建一个新的Element,不会复用。Widget.canUpdate()主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来禁止复用。【这就是我们常见的key的作用】

    1. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。

    1. “inactive”态的element不会显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成”active“状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。

    1. 如果element要重新插入到Element树的其它位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

    大多数情况下,只需要关注Widget树就行,Flutter框架已经将对Widget树的操作映射到Element树上,这可以极大的降低复杂度,提高开发效率。但是了解Element对理解整个Flutter UI框架是至关重要的,Flutter正是通过Element这个纽带将WidgetRenderObject关联起来,了解Element层不仅会帮助我们对Flutter UI框架有个清晰的认识,而且也会提高自己的抽象能力和设计能力。

Element的复用

从上面Element的生命周期的过程可以看出,说到Element的复用就必须要讲到Widget中的key,一般使用中,Widget 可以有 Stateful 和 Stateless 两种,两种widget构造函数中都有一个可选的参数Key,key是widget的标识符且能够帮助开发者在 Widget tree 中保存状态。

下面我们通过一个demo详细说明Key对于widget的作用机制

class StatelessDemo extends StatelessWidget {
  final randomValue = Random().nextInt(10000);
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Text('$randomValue');
  }
}

这是一个很简单的 Stateless Widget,在界面上显示一个随机数。 Random().nextInt(10000) 能够为这个 Widget 初始化一个小于10000的随机数。

将这个Widget展示到界面上:

class MyHomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _MyHomePageState();
  }
}

class _MyHomePageState extends State<MyHomePage> {
  List<StatelessDemo> widgetArr = [StatelessDemo(), StatelessDemo()];

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Padding(
      padding: EdgeInsets.only(top: 100),
      child: Column(
        children: [
          widgetArr[0],
          SizedBox(height: 50),
          widgetArr[1],
          SizedBox(height: 80),
          TextButton(
              onPressed: () {
                setState(() {
                  widgetArr.insert(0, widgetArr.removeAt(1));
                });
              },
              child: Text('交换widget位置'))
        ],
      ),
    );
  }
}

在界面展示了两个 StatelessDemo组件,当我们点击 TextButton 时,将会执行交换它们的顺序的操作。

现在我们做一点小小的改动,将这个 StatelessDemo 升级为 StatefulDemo:

class StatefulDemo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return StatefulDemoState();
  }
}

class StatefulDemoState extends State<StatefulDemo> {
  final randomValue = Random().nextInt(10000);
  
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Text('$randomValue');
  }
}

在 StatefulDemo 中,我们将定义 Random 和 build 方法都放进了 State 中。

现在我们还是使用刚才一样的布局,只不过把 StatelessDemo 替换成 StatefulDemo,看看会发生什么。

这时,无论我们怎样点击,都再也没有办法交换这两个widget的顺序了,而 setState 确实是被执行了的。

为了解决这个问题,我们在两个 Widget 构造的时候给它传入一个 UniqueKey:

class _MyHomePageState extends State<MyHomePage> {
  List<StatefulDemo> widgetArr = [
    StatefulDemo(key: UniqueKey()),
    StatefulDemo(key: UniqueKey())
  ];
  
  、、、、、、、、、
}

然后这两个 Widget 又可以正常被交换顺序了。

为什么 Stateful Widget 无法正常交换顺序,加上了 Key 之后就可以了,在这之中到底发生了什么? 为了弄明白这个问题,直接通过源码分析下,点击按钮调用setState方法,更新逻辑在Element的updateChild方法中,我们看下updateChild的源码:

  //用给定的新配置更新给定的子节点。
  @protected
  @pragma('vm:prefer-inline')
  Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
 	  // 如果'newWidget'为null,而'child'不为null,那么我们删除'child',返回null。
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    final Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      // When the type of a widget is changed between Stateful and Stateless via
      // hot reload, the element tree will end up in a partially invalid state.
      // That is, if the widget was a StatefulWidget and is now a StatelessWidget,
      // then the element tree currently contains a StatefulElement that is incorrectly
      // referencing a StatelessWidget (and likewise with StatelessElement).
      //
      // To avoid crashing due to type errors, we need to gently guide the invalid
      // element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition
      // returns false which prevents us from trying to update the existing element
      // incorrectly.
      //
      // For the case where the widget becomes Stateful, we also need to avoid
      // accessing `StatelessElement.widget` as the cast on the getter will
      // cause a type error to be thrown. Here we avoid that by short-circuiting
      // the `Widget.canUpdate` check once `hasSameSuperclass` is false.
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      if (hasSameSuperclass && child.widget == newWidget) {
       // 两个widget相同,而位置不同,那么更新child的位置,返回child。
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
      	 // 我们的交换例子的处理在这里,比较新旧widget的runtimeType和key是否同时相等,如果相等
         // 则复用Element,并更新Element的配置数据即widget。
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner!._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      } else {
        // 如果无法更新复用,child不为空,则先移除此child,然后创建一个新的Element并返回。
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      // 如果无法更新复用,child为空,那么创建一个新的Element并返回。
      newChild = inflateWidget(newWidget, newSlot);
    }

    assert(() {
      if (child != null)
        _debugRemoveGlobalKeyReservation(child);
      final Key? key = newWidget.key;
      if (key is GlobalKey) {
        assert(owner != null);
        owner!._debugReserveGlobalKeyFor(this, newChild, key);
      }
      return true;
    }());

    return newChild;
  }
  
//此方法为给定的widget创建一个Element,并将其作为该元素的子元素添加到给定的插槽中。
//该方法通常由[updateChild]调用,但可以由需要对创建Element进行更细粒度控制的子类直接调用。
//如果给定部件有一个gloablKey和一个已经存在的Element,它拥有一个小部件与全局的key,该函数将重用该元素(可能嫁接树中的另一个位置或重新激活它从活动列表中元素)而不是创建一个新的元素。' newSlot '参数指定该元素的[slot]的新值。
///这个函数返回的元素将被挂载,并处于“活跃”的生命周期状态。
  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        assert(newChild == updatedChild);
        return updatedChild;
      }
    }
    // 这里就调用到了createElement,重新创建了Element
    final Element newChild = newWidget.createElement();
    //将此元素添加到树中给定父元素的给定槽中。
    newChild.mount(this, newSlot);
    return newChild;
  }

  

WidgetcanUpdate方法:

/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
//新旧widget的runtimeType和key同时相等的时候,返回true,否则返回false

canUpdate方法的作用是判断newWidget是否可以替代oldWidget作为Element的配置。

该方法判断的依据就是runtimeTypekey是否相等。在我们上面的例子中,不管是StatelessWidget还是StatefulWidget的方块,显然canUpdate都会返回true。因此执行child.update(newWidget)方法,就是将持有的Widget更新了。但注意,这里并没有更新state。我们看一下StatefulWidget源码:

abstract class StatefulWidget extends Widget {

  const StatefulWidget({ Key key }) : super(key: key);
  
  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  State createState();
}

StatefulWidget中创建的是StatefulElement,它是Element的子类。

class StatefulElement extends ComponentElement {
	  /// Creates an element that uses the given widget as its configuration.
  StatefulElement(StatefulWidget widget)
      : state = widget.createState(),
        super(widget) {
    assert(() {
      if (!state._debugTypesAreRight(widget)) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
          ErrorDescription(
            'The createState function for ${widget.runtimeType} returned a state '
            'of type ${state.runtimeType}, which is not a subtype of '
            'State<${widget.runtimeType}>, violating the contract for createState.',
          ),
        ]);
      }
      return true;
    }());
    assert(state._element == null);
    state._element = this;
    assert(
      state._widget == null,
      'The createState function for $widget returned an old or invalid state '
      'instance: ${state._widget}, which is not null, violating the contract '
      'for createState.',
    );
    state._widget = widget;
    assert(state._debugLifecycleState == _StateLifecycle.created);
  }

  @override
  Widget build() => state.build(this);

  /// The [State] instance associated with this location in the tree.
  ///
  /// There is a one-to-one relationship between [State] objects and the
  /// [StatefulElement] objects that hold them. The [State] objects are created
  /// by [StatefulElement] in [mount].
  final State<StatefulWidget> state;
}

通过调用StatefulWidgetcreateElement方法,最终执行createState创建出state并持有。也就是说StatefulElement持有state。

明白了setState执行后的调用流程,我们来看下上面列子中的执行过程

  • StatelessDemo 执行过程:

在 StatelessDemo 中,两个 Widget 被交换了位置,此时我们并没有传入 key ,所以只比较它们的 runtimeType。这里 runtimeType 一致,canUpdate 方法返回 true,所以newWidget可以替代oldWidget作为Element的配置,Element 调用新持有 的Widget 的 build 方法重新构建,而我们的 randomValue 实际上就是储存在 widget 中的,因此在屏幕上两个 Widget 便被正确的交换了顺序。

  • StatefulDemo 比较过程:

在 StatefulDemo,我们将 randomValue 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 引用的是 Stateful Element

当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType,由于两个 Widget 的runtimeType相同,canUpdate 方法将会返回 true,于是两个 StatefulWidget 会交换位置。原有 Element 只会从它持有的 widget 的build 方法重新构建,注意此时更新的是widget,state并没有更新, 由于randomValue 的定义放在了 State 中,且由Element持有,所以randomValue不会交换,这里变换 StatefulWidget 的位置是没有作用的,因为randomValue由State持有,State又由Element持有。

但当给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key,此时两个Widget的runtimeType相同但key不同,所以 返回false。因为canUpdate返回false,此时不使用当前对应widget对element进行更新,而是根据当前对应的widget创建新的Element,创建新的Element的话,就会重新创建新state,randomValue 的定义放在了 State 中,看起来就像两个element交换了。

总结

当Element的配置数据改变时,Framework在决定重新创建Element前会先尝试复用相同位置旧的element,首先调用Widget的静态方法canUpdate(),,对比新旧widget是否相同,如果相同返回true,则复用旧Element,旧的Element会使用新的Widget配置数据更新,反之则会创建一个新的Element,不会复用。如果需要强制更新一个Widget时,可以通过指定不同的Key来禁止复用。

参考

说说Flutter中最熟悉的陌生人 —— Key