写点什么

Flutter 三棵树系列之详解各种 Key | 京东云技术团队

  • 2023-05-24
    北京
  • 本文字数:3626 字

    阅读完需:约 12 分钟

Flutter三棵树系列之详解各种Key | 京东云技术团队

简介

key 是 widget、element 和 semanticsNode 的唯一标识,同一个 parent 下的所有 element 的 key 不能重复,但是在特定条件下可以在不同 parent 下使用相同的 key,比如 page1 和 page2 都可以使用 ValueKey(1)


常用 key 的 UML 关系图如上,整体上 key 分为两大类-LocalKey 和 GlobalKey,这两个 key 都是抽象类,LocalKey 的实现类有 ValueKey、ObjectKey 和 UniqueKey,GlobalKey 实现类有 LabeledGlobalKey 和 GlobalObjectKey。

Key

@immutableabstract class Key {  const factory Key(String value) = ValueKey<String>;

@protected const Key.empty();}
复制代码


Key 是所有 key 的基类,内部实现了一个工厂构造函数,默认创建 String 类型的 ValueKey。内部还是先了一个 empty 的构造函数,主要是给子类用的。

LocalKey

abstract class LocalKey extends Key {  const LocalKey() : super.empty();}
复制代码


LocalKey 没有实际作用,主要是用来区分 GlobalKey 的,其具体的实现类有 ValueKey、ObjectKey、UniqueKey。

ValueKey

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

final T value;

@override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is ValueKey<T> && other.value == value; }

@override int get hashCode => hashValues(runtimeType, value);
复制代码


内部维护了泛型类型的 value 属性,并实现了==和 hashCode 方法。只要两个 ValueKey 的 value 属性相等,那么就认为两个 Key 相等。

ObjectKey

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

final Object? value;

@override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is ObjectKey && identical(other.value, value); }

@override int get hashCode => hashValues(runtimeType, identityHashCode(value));
复制代码


ObjectKey 是继承自 LocalKey 的,可以将其理解成泛型类型为 Object 的 ValueKey。但是注意两者的==方法是不一样的,ValueKey 根据 value 的值是否相等来判断 ValueKey 是否相等(相当于 java 的 equals 方法),而 ObjectKey 根据 indentical 方法(判断两个引用是否指向同一个对象,相当于 java 的==操作符)来判断两个 ObjectKey 是否相等的。

UniqueKey

class UniqueKey extends LocalKey {  UniqueKey();

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


唯一的 key,其并未重写==和 hashCode 方法,所有它只和自己相等。注意看 UniqueKey 的构造函数,并没有像上面介绍的几个 key 的构造函数一样使用 const 修饰,这样做的目的是为了进一步保证 UniqueKey 的唯一性。这样在调用 Element 的 updateChild 方法时,此方法内部调用的 Widget.canUpdate 方法就会始终返回 false,从而每次都会创建新的 child element。


所以,如果你想让某一个 widget 每一次都不复用 old element,而是去重新创建新的 element,那么就给他添加 UniqueKey 吧。


const 是编译时常量,在编译期,其值就已经确定。背后利用的类似于常量池的概念,被 const 修饰的对象会保存在常量池中,后面会对其进行复用。如果 UniqueKey 构造函数添加了 const 关键词,那么有如下代码 var k1 = const UniqueKey(); var k2 = const UniqueKey(); 此时 k1==k2 永远为 true,就不能保证其唯一性。

GlobalKey

GlobalKey 是全局唯一的,其默认实现是 LabeledGlobalKey,所以每次创建的都是新的 GlobalKey。所有的 GlobalKey 都保存在 BuildOwner 类中的一个 map 里,此 map 的 key 为 GlobalKey,此 map 的 value 则为 GlobalKey 关联的 element。


对于 GlobalKey,需要知道如下几点:


  • 当拥有 GlobalKey 的 widget 从 tree 的一个位置上移动到另一个位置时,需要 reparent 它的子树。为了 reparent 它的子树,必须在一个动画帧里完成从旧位置移动到新位置的操作。

  • 上面说到的 reparent 操作是昂贵的,因为要调用所有相关联的 State 和所有子节点的 deactive 方法,并且所有依赖 InheritedWidget 的 widget 去重建。

  • 不要在 build 方法里创建 GlobalKey,性能肯定不好,而且也容易出现意想不到的异常,比如子树里的 GestureDetector 可能会由于每次 build 时重新创建 GlobalKey 而无法继续追踪手势事件。

  • GlobalKey 提供了访问其关联的 Element 和 State 的方法。


下面看下其源码:


abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {  ///这里的debugLabel仅仅为了debug时使用  factory GlobalKey({ String? debugLabel }) => LabeledGlobalKey<T>(debugLabel);

///给子类使用的 const GlobalKey.constructor() : super.empty();

Element? get _currentElement => WidgetsBinding.instance!.buildOwner!._globalKeyRegistry[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;
复制代码


其和 Key 类差不多,也有一个工厂构造函数,默认创建的是 LabeledGlobalKey,其构造函数的 debugLabel 仅仅是为了 debug 时使用,并不会用来标识 element。


如何获取其关联的 element?从源码来看,其直接访问的是 BuildOwner 里用来保存 GlobalKey 和 Element 对应关系的 map。获取到了其关联的 element,那么就能获取到其对应的 widget 以及 state,详细的可以看上面的源码。


需要注意的是其并没有重写==和 hashCode 方法,构造函数也没有被 const 修饰,这也就使 LabeledGlobalKey 天然就是全局唯一的。

LabeledGlobalKey

这是 GlobalKey 的默认实现,内部仅有一个 debugLabel 属性,其他的也没啥。


class LabeledGlobalKey<T extends State<StatefulWidget>> extends GlobalKey<T> {  // ignore: prefer_const_constructors_in_immutables , never use const for this class  LabeledGlobalKey(this._debugLabel) : super.constructor();

final String? _debugLabel;}
复制代码

GlobalObjectKey

class GlobalObjectKey<T extends State<StatefulWidget>> extends GlobalKey<T> {  const GlobalObjectKey(this.value) : super.constructor();

final Object value;

@override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is GlobalObjectKey<T> && identical(other.value, value); }

@override int get hashCode => identityHashCode(value);
复制代码


特殊的 GlobalKey,重写了==和 hashCode 方法,内部维护了一个 Object 对象,通过判断此 Object 是否指向同一块内存地址来判断两个 GlobalObjectKey 是否相等。


GlobalKey 被要求全局唯一,其默认实现 LabeledGloalKey 因为其并没有重写==和 hashCode 方法,也不支持 const 构造函数,所以天然是全局唯一的。但是 GlobalObjectKey 不然,如果有两个或者多个地方使用到了拥有同一个 Object 的 GlobalObjectKey,那么就不能保证其全局唯一性,造成程序出错。此时,可以继承 GlobalObjectKey,实现一个 private 的内部类,比如:


class _MyGlobalObjectKey extends GlobalObjectKey {  const _MyGlobalObjectKey(Object value) : super(value);}
复制代码

总结

  • Flutter 里的 key 分为两类,一类是 LocalKey,实现类有 ValueKey、ObjectKey、UniqueKey;一类是 GlobalKey,实现类有 LabeledGlobalKey、GlobalObjectKey。

  • Key 是所有 keys 类的基类,其默认实现是 String 类型的 ValueKey。

  • 相同 parent 下的 key 是不能一样的,比如不能再同一个 page 里使用 VlaueKey(1),但是不同 parent 下是可以存在一样的 key 的,比如在两个界面里都使用 ValueKey(1)。

  • UniqueKey 只和自己相等,其并没有重写==和 hashCode 方法,也没有 const 修饰的构造函数。当调用 Element 的 updateChild 方法时,Widget.canUpdate 肯定返回 false,所以如果你想让 widget 每次都去创建新的 element 而不复用 old element,那么就给此 widget 使用 UniqueKey。

  • GlobalKey 的默认实现是 LabeledGlobalKey,其没有实现==和 hashCode 方法,也没有 const 修饰的构造函数,所以肯定能保证其全局唯一性。

  • 所有的 GlobalKey 都保存在 BuildOwner 类中,其内部维护了一个 map 用来保存 GlobalKey 与其对应的 Element。

  • GlobalObjectKey 是特殊的 GlobalKey,内部维护了一个 Object 属性,并实现了==和 hashCode 方法,通过判断 runtimeType 以及 Object 属性是否一致来判断两个 GlobalObjectKey 是否相等。

  • 使用 GlobalObjectKey 时,为了保证 GlobalObjectKey 的全局唯一性,最佳实践是继承自 GlobalObjectKey 实现一个 private 的内部类,可以有效避免多人开发时可能造成的 GlobalObjectKey 冲突的问题。


作者:京东物流 沈明亮

内容来源:京东云开发者社区

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Flutter三棵树系列之详解各种Key | 京东云技术团队_flutter_京东科技开发者_InfoQ写作社区