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

在这里插入图片描述

Key在Flutter的源码中能够说是无处不在,可是咱们平常中确不怎么使用它。有点像是“最熟悉的陌生人”,那么今天就来讲说这个“陌生人”,揭开它神秘的面纱。html

概念

KeyWidgetElementSemanticsNode的标识符。 只有当新的WidgetKey与当前ElementWidgetKey相同时,它才会被用来更新现有的ElementKey在具备相同父级的Element之间必须是惟一的。java

以上定义是源码中关于Key的解释。通俗的说就是Widget的标识,帮助实现Element的复用。关于它的说明源码中也提供了YouTube的视频连接:When to Use Keys。若是你没法访问,能够看Google 官方在优酷上传的git

例子

视频中的例子很简单且具备表明性,因此本文将采用它来介绍今天的内容。程序员

首先上代码:github

import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Widget> widgets;

  @override
  void initState() {
    super.initState();
    widgets = [
      StatelessColorfulTile(),
      StatelessColorfulTile()
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Row(
        children: widgets,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: _swapTile,
      ),
    );
  }

  _swapTile() {
    setState(() {
      widgets.insert(1, widgets.removeAt(0));
    });
  }
}

class StatelessColorfulTile extends StatelessWidget {

  final Color _color = Utils.randomColor();

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      width: 150,
      color: _color,
    );
  }
}

class Utils {
  static Color randomColor() {
    var red = Random.secure().nextInt(255);
    var greed = Random.secure().nextInt(255);
    var blue = Random.secure().nextInt(255);
    return Color.fromARGB(255, red, greed, blue);
  }
}
复制代码

代码能够直接复制到DartPad中运行查看效果。 或者点击这里直接运行算法

效果很简单,就是两个彩色方块,点击右下角的按钮后交换两个方块的位置。这里我就不放具体的效果图了。实际效果也和咱们预期的同样,两个方块成功交换位置。app

发现问题

上面的方块是StatelessWidget,那咱们把它换成StatefulWidget呢?。less

class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);

  @override
  StatefulColorfulTileState createState() => StatefulColorfulTileState();
}

class StatefulColorfulTileState extends State<StatefulColorfulTile> {
  final Color _color = Utils.randomColor();

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      width: 150,
      color: _color,
    );
  }
}
复制代码

再次执行代码,发现方块没有“交换”。这是为何?dom

在这里插入图片描述

分析问题

首先要知道Flutter中有三棵树,分别是==Widget Tree==、==Element Tree== 和 ==RenderObject Tree==。ide

  • Widget: Element配置信息。与Element的关系能够是一对多,一份配置能够创造多个Element实例。
  • Element:Widget 的实例化,内部持有WidgetRenderObject
  • RenderObject:负责渲染绘制

简单的比拟一下,Widget有点像是产品经理,规划产品整理需求。Element则是UI小姐姐,根据原型整理出最终设计图。RenderObject就是咱们程序员,负责具体的落地实现。

代码中能够肯定一点,两个方块的Widget确定是交换了。既然Widget没有问题,那就看看Element

可是为何StatelessWidget能够成功,换成StatefulWidget就失效了?

点击按钮调用setState方法,依次执行:

graph TB
A["_element.markNeedsBuild()"] -- 标记自身元素dirty为true --> B["owner.scheduleBuildFor()"]
B --添加至_dirtyElements--> D["drawFrame()"] 
D --> E["buildScope()"]
E --> F["_dirtyElements[index].rebuild()"]
F --> G["performRebuild()"]
G --> H["updateChild()"]
复制代码

咱们重点看一下ElementupdateChild方法:

@protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  	// 若是'newWidget'为null,而'child'不为null,那么咱们删除'child',返回null。
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    if (child != null) {
      // 两个widget相同,位置不一样更新位置,返回child。这里比较的是hashCode
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      // 咱们的交换例子处理在这里
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      }
      deactivateChild(child);
    }
    // 若是没法更新复用,那么建立一个新的Element并返回。
    return inflateWidget(newWidget, newSlot);
  }

复制代码

WidgetcanUpdate方法:

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
复制代码

这里出现了咱们今天的主角Key,不过咱们先放在一边。canUpdate方法的做用是判断newWidget是否能够替代oldWidget做为Element的配置。 一开始也提到了,Element会持有Widget。

该方法判断的依据就是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 {

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    _state._element = this;  
    _state._widget = widget;
  }

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

  State<StatefulWidget> get state => _state;
  State<StatefulWidget> _state;
  ...
}
复制代码

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

因此咱们上面两个StatefulWidget的方块的交换,实际只是交换了“身体”,而“灵魂”没有交换。因此无论你怎么点击按钮都是没有变化的。

解决问题

找到了缘由,那么怎么解决它?那就是设置一个不一样的Key

@override
  void initState() {
    super.initState();
    widgets = [
      StatefulColorfulTile(key: const Key("1")), StatefulColorfulTile(key: const Key("2")) ];
  }
复制代码

可是这里要注意的是,这里不是说添加key之后,在canUpdate方法返回false,最后执行inflateWidget(newWidget, newSlot)方法建立新的Element。(不少相关文章对于此处的说明都有误区。。。好吧我认可我一开始也被误导了。。。)

@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;
  }
复制代码

若是如此,那么执行createElement方法势必会从新建立state,那么方块的颜色也就随机变了。固然此种状况并非不存在,好比咱们给现有的方块外包一层PaddingSingleChildRenderObjectElement):

@override
  void initState() {
    super.initState();
    widgets = [
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: StatefulColorfulTile(key: Key("1"),)
      ),
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: StatefulColorfulTile(key: Key("2"),)
      ),
    ];
  }
复制代码

这种状况下,交换后比较外层Padding不变,接着比较内层StatefulColorfulTile,由于key不相同致使颜色随机改变。由于两个方块位于不一样子树,二者在逐层对比中用到的就是canUpdate方法返回false来更改。

而本例是方块的外层是RowMultiChildRenderObjectElement),是对比两个List,存在不一样。关键在于update时调用的RenderObjectElement.updateChildren方法。

@protected
  List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
  	...
    int newChildrenTop = 0;
    int oldChildrenTop = 0;
    int newChildrenBottom = newWidgets.length - 1;
    int oldChildrenBottom = oldChildren.length - 1;

    final List<Element> newChildren = oldChildren.length == newWidgets.length ?
        oldChildren : List<Element>(newWidgets.length);

    Element previousChild;

    // 从前日后依次对比,相同的更新Element,记录位置,直到不相等时跳出循环。
    while ((oldChildrenTop <= oldChildrenBottom) && 
    	(newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
      final Widget newWidget = newWidgets[newChildrenTop];
      // 注意这里的canUpdate,本例中在没有添加key时返回true。
      // 所以直接执行updateChild,本循环结束返回newChildren。后面因条件不知足都在不执行。
      // 一旦添加key,这里返回false,不一样之处就此开始。
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    }

    // 从后往前依次对比,记录位置,直到不相等时跳出循环。
    while ((oldChildrenTop <= oldChildrenBottom) && 
    	(newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
      final Widget newWidget = newWidgets[newChildrenBottom];
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
        break;
      oldChildrenBottom -= 1;
      newChildrenBottom -= 1;
    }
	// 至此,就能够获得新旧List中不一样Weiget的范围。
    final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
    Map<Key, Element> oldKeyedChildren;
    // 若是存在中间范围,扫描旧children,获取全部的key与Element保存至oldKeyedChildren。
    if (haveOldChildren) {
      oldKeyedChildren = <Key, Element>{};
      while (oldChildrenTop <= oldChildrenBottom) {
        final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
        if (oldChild != null) {
          if (oldChild.widget.key != null)
            oldKeyedChildren[oldChild.widget.key] = oldChild;
          else
          	// 没有key就移除对应的Element
            deactivateChild(oldChild);
        }
        oldChildrenTop += 1;
      }
    }
	// 更新中间不一样的部分
    while (newChildrenTop <= newChildrenBottom) {
      Element oldChild;
      final Widget newWidget = newWidgets[newChildrenTop];
      if (haveOldChildren) {
        final Key key = newWidget.key;
        if (key != null) {
          // key不为null,经过key获取对应的旧Element
          oldChild = oldKeyedChildren[key];
          if (oldChild != null) {
            if (Widget.canUpdate(oldChild.widget, newWidget)) {
              oldKeyedChildren.remove(key);
            } else {
              oldChild = null;
            }
          }
        }
      }
      // 本例中这里的oldChild.widget与newWidget hashCode相同,在updateChild中成功被复用。
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
    }
    
    // 重置
    newChildrenBottom = newWidgets.length - 1;
    oldChildrenBottom = oldChildren.length - 1;

    // 将后面相同的Element更新后添加到newChildren,至此造成新的完整的children。
    while ((oldChildrenTop <= oldChildrenBottom) && 
    	(newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = oldChildren[oldChildrenTop];
      final Widget newWidget = newWidgets[newChildrenTop];
      final Element newChild = updateChild(oldChild, newWidget, previousChild);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    }

    // 清除旧列表中多余的Element
    if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
      for (Element oldChild in oldKeyedChildren.values) {
        if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
          deactivateChild(oldChild);
      }
    }

    return newChildren;
  }
复制代码

这个方法有点复杂,详细的执行流程我在代码中添加了注释。看完这个diff算法,只能说一句:妙啊!!

到此也就解释了咱们一开始提出的问题。不知道你对这不起眼的key是否是有了更深的认识。经过上面的例子能够总结如下三点:

  • 通常状况下不设置key也会默认复用Element

  • 对于更改同一父级下Widget(尤为是runtimeType不一样的Widget)的顺序或是增删,使用key能够更好的复用Element提高性能

  • StatefulWidget使用key,能够在发生变化时保持state。不至于发生本例中“身体交换”的bug。

Key的种类

上面例子中咱们用到了Key,其实它还有许多种类。

在这里插入图片描述

1.LocalKey

LocalKey 继承自 Key,在同一父级的Element之间必须是惟一的。(固然了,你要是写成不惟一也行,不事后果自负哈。。。)

咱们基本不直接使用LocalKey ,而是使用的它的子类:

ValueKey

咱们上面使用到的Key,其实就是ValueKey<String>。它主要是使用特定类型的值来作标识的,像是“值引用”,好比int、String等类型。咱们看它源码中的 ==操做符方法:

class ValueKey<T> extends LocalKey {
  const ValueKey(this.value);
  
  final T value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ValueKey<T> typedOther = other;
    return value == typedOther.value; // <---
  }
  ...
}
复制代码

ObjectKey

有“值引用”,就有“对象引用”。主要仍是==操做符方法:

class ObjectKey extends LocalKey {
  const ObjectKey(this.value);

  final Object value;

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final ObjectKey typedOther = other;
    return identical(value, typedOther.value); // <---
  }
  ...
}
复制代码

UniqueKey

会生成一个独一无二的key值。

class UniqueKey extends LocalKey {
  UniqueKey();

  @override
  String toString() => '[#${shortHash(this)}]';
}

String shortHash(Object object) {
  return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0');
}

复制代码

PageStorageKey

用于保存和还原比Widget生命周期更长的值。好比用于保存滚动的偏移量。每次滚动完成时,PageStorage会保存其滚动偏移量。 这样在从新建立Widget时能够恢复以前的滚动位置。相似的,在ExpansionTile中用于保存展开与闭合的状态。

具体的实现原理也很简单,看看PageStorage的源码就清楚了,这里就不展开了。

2.GlobalKey

介绍

GlobalKey 也继承自 Key,在整个应用程序中必须是惟一的。GlobalKey源码有点长,我就不所有贴过来了。

@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
  factory GlobalKey({ String debugLabel }) => LabeledGlobalKey<T>(debugLabel);

  const GlobalKey.constructor() : super.empty();

  static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
  // 在`Element的 `mount`中注册GlobalKey。
  void _register(Element element) {
    _registry[this] = element;
  }
  // 在`Element的 `unmount`中注销GlobalKey。
  void _unregister(Element element) {
    if (_registry[this] == element)
      _registry.remove(this);
  }

  Element get _currentElement => _registry[this];

  BuildContext get currentContext => _currentElement;
  
  Widget get currentWidget => _currentElement?.widget;

  T get currentState {
    final Element element = _currentElement;
    if (element is StatefulElement) {
      final StatefulElement statefulElement = element;
      final State state = statefulElement.state;
      if (state is T)
        return state;
    }
    return null;
  }
  ...
}

复制代码

它的内部存在一个Map<GlobalKey, Element>的静态Map,经过调用_register_unregister方法来添加和删除Element。同时它的内部还持有当前的ElementWidget甚至State。能够看到 GlobalKey是很是昂贵的,没有特别的复用需求,不建议使用它

怎么复用呢?GlobalKey在上面inflateWidget的源码中出现过一次。当发现key是GlobalKey时,使用_retakeInactiveElement方法复用Element

Element _retakeInactiveElement(GlobalKey key, Widget newWidget) {
    final Element element = key._currentElement;
    if (element == null)
      return null;
    if (!Widget.canUpdate(element.widget, newWidget))
      return null;
    final Element parent = element._parent;
    if (parent != null) {
      parent.forgetChild(element);
      parent.deactivateChild(element);
    }
    owner._inactiveElements.remove(element);
    return element;
  }

复制代码

若是获取到了Element,那么就从旧的节点上移除并返回。不然将在inflateWidget从新建立新的Element

使用

  • 首先就是上面提到的使用相同的GlobalKey来实现复用。

  • 利用GlobalKey持有的BuildContext。好比常见的使用就是获取Widget的宽高信息,经过BuildContext能够在其中获取RenderObjectSize,从而拿到宽高信息。这里就不贴代码了,有须要能够看此处示例

  • 利用GlobalKey持有的State,实如今外部调用StatefulWidget内部方法。好比经常使用GlobalKey<NavigatorState>来实现无Context跳转页面,在点击推送信息跳转指定页面就须要用到。

先建立一个GlobalKey<NavigatorState>

static GlobalKey<NavigatorState> navigatorKey = new GlobalKey();
复制代码

添加至MaterialApp:

MaterialApp(
   navigatorKey: navigatorKey,
   ...
  );
复制代码

而后就是调用push方法:

navigatorKey.currentState.push(MaterialPageRoute(
    builder: (BuildContext context) => MyPage(),
  ));
复制代码

经过GlobalKey持有的State,就能够调用其中的方法、获取数据。

LabeledGlobalKey

它是一个带有标签的GlobalKey。 该标签仅用于调试,不用于比较。

GlobalObjectKey

同上ObjectKey。区别在于它是GlobalKey

思考题

最后来个思考题:对于可选参数key,我搜索了一下Flutter的源码。发现只有Dismissible这个滑动删除组件要求必须传入key。结合今天的内容,想一想是为何?若是传入相同的key,会发生什么?


本篇是“说说”系列第三篇,前两篇连接奉上:

PS:此系列都是本身的学习记录与总结,尽力作到“通俗易懂”和“看着一篇就够了”。不过也不现实,学习之路没有捷径。

写着写着,就写的有点多了。本想着拆成两篇,想一想算了。毕竟我是一名月更选手,哈哈~~

若是本文对你有所帮助或启发的话,还请不吝点赞收藏支持一波。同时也多多支持个人Flutter开源项目flutter_deer

咱们下个月见~~

相关文章
相关标签/搜索