Flutter GlobalKey 详解
引言
在 Flutter 开发中,GlobalKey 是一个常用但容易被误解的机制。它允许开发者跨 Widget 树访问 Element 和 State,同时也是列表动画、Form 表单校验、Navigator 等高级功能的基础。但 GlobalKey 的相等性为什么基于对象标识?为什么 reparenting(重定父)必须在同一帧内完成?为什么 deactivate 不注销 GlobalKey?
本文将从 Flutter Framework 源码出发,逐层分析 GlobalKey 的实现原理,覆盖 Key 类型体系、全局注册表、Element 生命周期中的注册/注销流程,以及最核心的跨树重定父机制。
1. Key 类型体系
在深入 GlobalKey 之前,需要先理解 Key 的整体类型层次。源码位于 packages/flutter/lib/src/foundation/key.dart:
// packages/flutter/lib/src/foundation/key.dart
abstract class Key {
// factory 构造器,直接创建一个 ValueKey<String>
const factory Key(String value) = ValueKey<String>;
// 子类使用的默认构造器
@protected
const Key.empty();
}
Key 本身是一个抽象类,但其 factory 构造器让开发者可以简便地写 Key('myKey'),它实际创建的是一个 ValueKey<String>。
Key 体系分为两大分支:LocalKey 和 GlobalKey。
1.1 LocalKey:父子节点内唯一
LocalKey 在同一父节点下是唯一的(不同父节点之间可以重复):
// packages/flutter/lib/src/foundation/key.dart
abstract class LocalKey extends Key {
const LocalKey() : super.empty();
}
LocalKey 有三个具体实现:
ValueKey<T> — 基于值判等,最常用:
// packages/flutter/lib/src/foundation/key.dart
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 => Object.hash(runtimeType, value);
}
注意 operator == 使用 == 而非 identical,因此两个不同的 ValueKey('foo') 实例是相等的。此外,runtimeType 参与了判等——如果你创建了一个私有子类 class _MyKey extends ValueKey<int>,它与 ValueKey<int> 即使值相同也不会匹配。这是一种天然的「命名空间」隔离。
ObjectKey — 基于 identityHashCode 判等:
// packages/flutter/lib/src/foundation/key.dart
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 => Object.hash(runtimeType, identityHashCode(value));
}
即使两个对象通过 == 相等,只要不是同一个实例,ObjectKey 也认为它们不同。
UniqueKey — 每个实例唯一:
// packages/flutter/lib/src/foundation/key.dart
class UniqueKey extends LocalKey {
UniqueKey(); // 不使用 const 构造器!
@override
String toString() => '[#${shortHash(this)}]';
}
UniqueKey 不使用 const 构造器,因此每次 UniqueKey() 调用都会创建一个全新对象实例。它依赖于 Dart 的默认对象标识判等(即 identical)。
1.2 GlobalKey:全应用唯一
GlobalKey 的类层次与 LocalKey 不同——它定义在 packages/flutter/lib/src/widgets/framework.dart,而不是 key.dart。原因在后面会解释。
// packages/flutter/lib/src/widgets/framework.dart
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
// factory 实际创建 LabeledGlobalKey
factory GlobalKey({String? debugLabel}) => LabeledGlobalKey<T>(debugLabel);
// 子类使用
const GlobalKey.constructor() : super.empty();
}
它的两个具体子类是:
LabeledGlobalKey<T>:factory 的默认产物,可附带调试标签。GlobalObjectKey<T>:基于identical对 value 对象进行判等。
// packages/flutter/lib/src/widgets/framework.dart
class LabeledGlobalKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
LabeledGlobalKey(this._debugLabel) : super.constructor();
final String? _debugLabel;
// toString() 包含 debugLabel,用于调试
}
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() 和 LabeledGlobalKey 都使用 Dart 默认的对象标识判等(同一个对象实例才相等)。而 GlobalObjectKey 使用 identical 判等其 value,这意味着多个 GlobalObjectKey(sameObject) 实例是相等的。这个设计允许将 Widget 的身份绑定到某个外部对象的 identity 上。
为什么 GlobalKey 定义在 framework.dart 而非 key.dart?
因为 GlobalKey 需要通过 WidgetsBinding.instance.buildOwner!._globalKeyRegistry 访问全局注册表,引入了对 widgets 层的依赖。而 key.dart 位于 foundation 层,不应依赖 widgets 层。这是典型的架构分层约束。
2. GlobalKey 的对外 API:currentState / currentContext / currentWidget
GlobalKey 最常用的场景是通过 currentState 访问远端 Widget 的 State:
// packages/flutter/lib/src/widgets/framework.dart
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
// 核心:从全局注册表查找当前关联的 Element
Element? get _currentElement =>
WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];
BuildContext? get currentContext => _currentElement;
Widget? get currentWidget => _currentElement?.widget;
T? get currentState => switch (_currentElement) {
StatefulElement(:final T state) => state,
_ => null,
};
}
所有对外 API 都建立在 _currentElement 之上,而 _currentElement 的实现极其简洁——就是一个 Map 查找。这里使用了 Dart 3 的 record pattern 匹配:只有当 _currentElement 是 StatefulElement 且其 state 是类型 T 的子类型时,才返回 state,否则返回 null。
这意味着:
- 如果树中没有元素持有该 GlobalKey,所有 getter 返回 null。
- 如果持有该 key 的是 StatelessWidget,
currentState返回 null(因为不是 StatefulElement),但currentContext和currentWidget仍可用。 GlobalKey<YourState>的类型参数提供了类型安全的 State 访问。
3. 全局注册表:_globalKeyRegistry
GlobalKey 之所以「全局」,核心在于 BuildOwner 中维护的一个 Map:
// packages/flutter/lib/src/widgets/framework.dart
class BuildOwner {
// 唯一真相来源:GlobalKey → Element 的映射
final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};
// 公开的计数统计
int get globalKeyCount => _globalKeyRegistry.length;
}
这个 Map 是 Map<GlobalKey, Element>,key 是 GlobalKey 对象,value 是该 key 当前关联的 Element。由于 GlobalKey 的相等性默认基于 Dart 对象标识(identical),这意味着同一个 Dart 对象实例只能对应一个 Element——这正是「全应用唯一」的语义来源。
BuildOwner 实例由 WidgetsFlutterBinding 持有,一个应用中只有一个 WidgetsFlutterBinding(单例),因此注册表是全局的。
3.1 注册方法:_registerGlobalKey
// packages/flutter/lib/src/widgets/framework.dart
void _registerGlobalKey(GlobalKey key, Element element) {
assert(() {
// Debug 模式:检测重复注册
if (_globalKeyRegistry.containsKey(key)) {
final Element oldElement = _globalKeyRegistry[key]!;
// 只有不同类型才报错(同类型覆盖在 reparenting 时是合法的)
assert(element.widget.runtimeType != oldElement.widget.runtimeType);
// 记录被覆盖的旧元素,稍后在 finalizeTree 中报错
_debugIllFatedElements?.add(oldElement);
}
return true;
}());
// 无条件写入
_globalKeyRegistry[key] = element;
}
注册逻辑很直接:将 (key, element) 写入 map。在 debug 模式下,如果发现已有同 key 的 element 且类型不同,会记录到 _debugIllFatedElements 中,在 finalizeTree 时统一报错。
3.2 注销方法:_unregisterGlobalKey
// packages/flutter/lib/src/widgets/framework.dart
void _unregisterGlobalKey(GlobalKey key, Element element) {
assert(() {
if (_globalKeyRegistry.containsKey(key) &&
_globalKeyRegistry[key] != element) {
final Element oldElement = _globalKeyRegistry[key]!;
assert(element.widget.runtimeType != oldElement.widget.runtimeType);
}
return true;
}());
// 只有当前持有者是 element 时才删除(安全守卫)
if (_globalKeyRegistry[key] == element) {
_globalKeyRegistry.remove(key);
}
}
这里有一个关键的安全守卫:_globalKeyRegistry[key] == element。只有当注册表中该 key 对应的元素确实是当前要注销的元素时,才执行删除。这个守卫防止了以下故障场景:Element A 持有 key,但在异常路径下 key 已被 Element B 覆盖,此时 Element A 尝试注销不会误删 Element B 的绑定。
4. Element 生命周期中的 GlobalKey 注册与注销
GlobalKey 的注册和注销嵌入在 Element 的生命周期中。Flutter 的 Element 有以下几种生命周期状态(定义在 packages/flutter/lib/src/widgets/framework.dart 的 _ElementLifecycle 枚举中):
- initial:元素刚创建,尚未挂载。
- active:元素在树中,参与构建、布局、绘制。
- inactive:元素被移出树,但可能被重新激活(reparenting 的时间窗口)。
- defunct:元素被彻底销毁,不可逆。
4.1 mount:注册 GlobalKey
当一个新的 Element 首次被挂载到树上时,框架调用 Element.mount:
// packages/flutter/lib/src/widgets/framework.dart
void mount(Element? parent, Object? newSlot) {
// ... 初始化 _parent, _slot, _lifecycleState, _depth, _owner 等
_lifecycleState = _ElementLifecycle.active;
// ...
final Key? key = widget.key;
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this); // 注册到全局表
}
_updateInheritance();
attachNotificationTree();
}
注意:只有首次挂载(mount)时才注册 GlobalKey。如果一个元素是通过 reparenting 重新激活的(_activateWithParent → activate),它不会重新注册——因为它在 deactivate 时并未注销。
4.2 unmount:注销 GlobalKey
当元素被确认不再需要时,框架调用 Element.unmount:
// packages/flutter/lib/src/widgets/framework.dart
void unmount() {
assert(_lifecycleState == _ElementLifecycle.inactive);
// ...
final Key? key = _widget?.key;
if (key is GlobalKey) {
owner!._unregisterGlobalKey(key, this); // 从全局表移除
}
_widget = null;
_dependencies = null;
_lifecycleState = _ElementLifecycle.defunct;
}
关键设计:deactivate 不注销 GlobalKey。 当一个元素被移出树(deactivate → 状态变为 inactive),GlobalKey 映射保持不变。只有在 unmount(状态变为 defunct,不可逆销毁)时才注销。这个设计是 reparenting 机制的基础——如果 deactivate 时就注销,那么新位置就无法通过 GlobalKey 找到这个元素了。
5. 重定父(Reparenting)机制
这是 GlobalKey 最复杂也最精妙的部分。Reparenting 允许持有相同 GlobalKey 的 Widget 在同一帧内从一个父节点「移动」到另一个父节点,而不会销毁重建整个 Element 子树。
5.1 触发链路:从 WidgetsBinding.drawFrame 到 inflateWidget
每次帧刷新时,WidgetsBinding.drawFrame 调用流程如下(源码位于 packages/flutter/lib/src/widgets/binding.dart):
// packages/flutter/lib/src/widgets/binding.dart
void drawFrame() {
// ...
if (rootElement != null) {
buildOwner!.buildScope(rootElement!); // 执行所有 dirty element 的 rebuild
}
super.drawFrame(); // 调用 RendererBinding.drawFrame(layout → paint)
buildOwner!.finalizeTree(); // 清理 inactive 元素
}
当某个父 Element rebuild 后,它调用 updateChild 来同步新旧 widget 列表。updateChild 是 Element 树 diff 的核心:
// packages/flutter/lib/src/widgets/framework.dart
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (newWidget == null) {
if (child != null) {
deactivateChild(child); // 旧有子元素,新无 → 移除
}
return null;
}
final Element newChild;
if (child != null) {
// 尝试复用:类型相同 + key 相同 + widget 可更新
if (hasSameSuperclass && child.widget == newWidget) {
// widget 未变,只更新 slot
if (child.slot != newSlot) updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
// widget 类型相同、key 相同,但配置变了 → update
if (child.slot != newSlot) updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
// 无法复用 → deactivate 旧的,inflate 新的
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
// 旧无子元素,新有 → 创建
newChild = inflateWidget(newWidget, newSlot);
}
// Debug 模式:记录 GlobalKey 的父-子关系
assert(() {
if (child != null) _debugRemoveGlobalKeyReservation(child);
final Key? key = newWidget.key;
if (key is GlobalKey) {
owner!._debugReserveGlobalKeyFor(this, newChild, key);
}
return true;
}());
return newChild;
}
Widget.canUpdate 的定义是类型匹配 + key 匹配:
// packages/flutter/lib/src/widgets/framework.dart
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
对于 GlobalKey,oldWidget.key == newWidget.key 意味着两个 Widget 持有同一个 GlobalKey 对象实例(因为默认使用 Dart 对象标识判等)。这是 reparenting 能发生的先决条件。
5.2 inflateWidget:重定父的总入口
inflateWidget 是创建或复用 Element 的统一入口。当 GlobalKey 存在时,它首先尝试「抢夺」已有元素,而不是创建新的:
// packages/flutter/lib/src/widgets/framework.dart
Element inflateWidget(Widget newWidget, Object? newSlot) {
// ...
final Key? key = newWidget.key;
final Element? inactiveChild = key is GlobalKey
? _retakeInactiveElement(key, newWidget) // 尝试抢夺
: null;
final Element newChild = inactiveChild ?? newWidget.createElement();
if (inactiveChild != null) {
// 抢夺成功:激活已有元素
inactiveChild._activateWithParent(this, newSlot);
final Element? updatedChild = updateChild(inactiveChild, newWidget, newSlot);
return updatedChild!;
} else {
// 抢夺失败:创建新元素并挂载
newChild.mount(this, newSlot);
return newChild;
}
}
这里有两条路径:
- 无 GlobalKey 或抢夺失败 → 调用
newWidget.createElement()创建新元素 →mount(注册 GlobalKey)。 - 抢夺成功 →
_activateWithParent激活到新父节点 →updateChild同步 widget。
5.3 _retakeInactiveElement:抢夺已有元素
这是 reparenting 的灵魂。它的职责是:通过 GlobalKey 找到已有 Element,将其从旧位置剥离,返回给新父节点使用。
// packages/flutter/lib/src/widgets/framework.dart
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) {
assert(() {
// 同一父节点下出现两个相同 GlobalKey → 错误
if (parent == this) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
"A GlobalKey was used multiple times inside one widget's child list."
),
// ... 错误详情
]);
}
// Debug:记录旧父节点需要 rebuild
parent.owner!._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
parent, key,
);
return true;
}());
// 从旧父节点剥离
parent.forgetChild(element); // 通知旧父「忘记」这个子节点
parent.deactivateChild(element); // 正式脱离
}
assert(element._parent == null); // 此时应已无父节点
// 第四步:从 inactive 集合中取出
owner!._inactiveElements.remove(element);
return element;
}
分四种情况讨论:
情况 A:key._currentElement == null → 这是该 GlobalKey 首次使用,返回 null,走创建新元素路径。
情况 B:类型不兼容 → 返回 null,旧元素将被 unmount,新元素被创建。例如:原本的 Widget 是 Container,新 Widget 是 Text,类型不同,无法更新。
情况 C:元素已有父节点(_parent != null)→ 说明旧父节点还未 rebuild,需要主动抢夺:
- 若
parent == this:同一个父节点下出现两个相同 GlobalKey → 抛出异常。 - 否则:调用
parent.forgetChild(element)+parent.deactivateChild(element),将元素从旧父剥离并置入 inactive 状态。
情况 D:元素无父节点(_parent == null)→ 说明旧父节点已 rebuild 并将元素 deactivate,元素已在 inactive 集合中,直接从集合取出即可。
5.4 forgetChild 与 deactivateChild:从旧父节点脱离
forgetChild:通知旧父「这个孩子被抢走了」
// packages/flutter/lib/src/widgets/framework.dart
void forgetChild(Element child) {
assert(() {
if (child.widget.key is GlobalKey) {
_debugForgottenChildrenWithGlobalKey?.add(child);
}
return true;
}());
}
forgetChild 本身是一个抽象方法,框架期望子类在实现时从子模型中移除该子节点。debug 模式下,它额外记录被遗忘的 GlobalKey 子节点,供后续重复检测使用。
注释中特别说明:「We cannot remove the global key reservation directly in this method because the forgotten child is not removed from the tree until this Element is updated in update.」这意味着在旧父节点调用 update 之前,它仍然「认为」自己有这个孩子,只有当 update 被调用时才正式清除 debug reservation。
deactivateChild:正式将元素移入 inactive 状态
// packages/flutter/lib/src/widgets/framework.dart
void deactivateChild(Element child) {
assert(child._parent == this);
child._parent = null; // 切断父子关系
child.detachRenderObject(); // 从渲染树脱离
owner!._inactiveElements.add(child); // 加入 inactive 集合
// _inactiveElements.add 内部会递归调用 child.deactivate()
}
_inactiveElements.add 会递归地调用 deactivate() 和子节点的 deactivate(),触发 State.deactivate() 回调。
5.5 _activateWithParent:绑定到新父节点
// packages/flutter/lib/src/widgets/framework.dart
void _activateWithParent(Element parent, Object? newSlot) {
assert(_lifecycleState == _ElementLifecycle.inactive);
_parent = parent; // 设置新父节点
_owner = parent.owner; // 设置新 BuildOwner
_updateDepth(_parent!.depth); // 递归更新深度
_updateBuildScopeRecursively(); // 递归更新构建作用域
_activateRecursively(this); // 递归激活整个子树
attachRenderObject(newSlot); // 挂载到渲染树新位置
assert(_lifecycleState == _ElementLifecycle.active);
}
_activateRecursively 的核心是调用每个元素的 activate():
// packages/flutter/lib/src/widgets/framework.dart
static void _activateRecursively(Element element) {
assert(element._lifecycleState == _ElementLifecycle.inactive);
element.activate();
assert(element._lifecycleState == _ElementLifecycle.active);
element.visitChildren(_activateRecursively);
}
而 activate() 方法本身:
// packages/flutter/lib/src/widgets/framework.dart
void activate() {
assert(_lifecycleState == _ElementLifecycle.inactive);
final bool hadDependencies =
(_dependencies?.isNotEmpty ?? false) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance(); // 重新建立 InheritedWidget 依赖
attachNotificationTree();
if (_dirty) {
owner!.scheduleBuildFor(this); // 如果之前在 dirty 列表,重新调度
}
if (hadDependencies) {
didChangeDependencies(); // 通知依赖变化
}
}
注意 _dependencies?.clear():deactivate 时元素从 InheritedElement 的解依赖操作已完成,但依赖列表被保留用于判断是否需要触发 didChangeDependencies。activate 时正式清空列表并重新建立依赖。
5.6 updateChild:同步 Widget 配置
从 inflateWidget 的流程回到 updateChild:即使元素来自 reparenting,最后仍要调用 updateChild(inactiveChild, newWidget, newSlot):
// 在 inflateWidget 中
inactiveChild._activateWithParent(this, newSlot);
final Element? updatedChild = updateChild(inactiveChild, newWidget, newSlot);
由于 _retakeInactiveElement 已经验证了 Widget.canUpdate,updateChild 会走「可更新」分支,调用 child.update(newWidget):
// packages/flutter/lib/src/widgets/framework.dart
void update(covariant Widget newWidget) {
assert(_lifecycleState == _ElementLifecycle.active);
// 清除 debug 模式下记录的「被遗忘子节点」的 GlobalKey 预留
assert(() {
_debugForgottenChildrenWithGlobalKey
?.forEach(_debugRemoveGlobalKeyReservation);
_debugForgottenChildrenWithGlobalKey?.clear();
return true;
}());
_widget = newWidget;
}
update 简单地将 _widget 替换为新 widget,同时清除 debug 模式下记录的 GlobalKey 预留信息。
5.7 完整时序总结
假设 Widget A 持有 GlobalKey #123,要从父节点 OldParent 移动到 NewParent:
帧开始
│
├─ OldParent rebuild
│ │ 新旧 widget 列表比较
│ │ 发现原来 #123 的位置现在是 null
│ │
│ └─ updateChild(oldChild_with_#123, null, ...)
│ └─ deactivateChild(element_#123)
│ ├─ element._parent = null
│ ├─ detachRenderObject()
│ └─ _inactiveElements.add(element_#123)
│ └─ _deactivateRecursively(element_#123)
│ ├─ element.deactivate()
│ │ └─ _ensureDeactivated()
│ │ ├─ 从 InheritedElement 解依赖
│ │ └─ lifecycle → inactive
│ └─ 递归所有子节点
│ ⚠ GlobalKey 注册表不变!element_#123 仍在表中
│
├─ NewParent rebuild
│ │ 新旧 widget 列表比较
│ │ 发现 #123 位置出现新 widget
│ │
│ └─ updateChild(null, newWidget_with_#123, newSlot)
│ └─ inflateWidget(newWidget, newSlot)
│ ├─ key is GlobalKey → _retakeInactiveElement(#123, newWidget)
│ │ ├─ key._currentElement → 找到 element_#123(注册表中!)
│ │ ├─ Widget.canUpdate → true
│ │ ├─ element._parent == null(已被 OldParent 设为 null)
│ │ └─ _inactiveElements.remove(element_#123)
│ │
│ └─ _activateWithParent(this, newSlot)
│ ├─ _parent = NewParent
│ ├─ _owner = NewParent.owner
│ ├─ _updateDepth(...)
│ ├─ _updateBuildScopeRecursively()
│ ├─ _activateRecursively(element_#123)
│ │ └─ element.activate()
│ │ ├─ lifecycle → active
│ │ ├─ _updateInheritance()
│ │ └─ 若有依赖 → didChangeDependencies()
│ └─ attachRenderObject(newSlot)
│
├─ super.drawFrame() (RendererBinding:layout → paint)
│
└─ finalizeTree()
├─ _inactiveElements._unmountAll()
│ └─ 清理所有未被重用的 inactive 元素
│ └─ element.unmount()
│ └─ _unregisterGlobalKey() ← 只有这里才注销
└─ Debug 校验(重复检测)
如果是 NewParent 先 rebuild(OldParent 尚未 rebuild):
NewParent rebuild → inflateWidget
└─ _retakeInactiveElement(#123, newWidget)
├─ element._parent == OldParent ← 元素还在旧树上!
├─ parent != null && parent != this → 进入抢夺路径
├─ parent.forgetChild(element_#123) ← 让 OldParent「忘记」
└─ parent.deactivateChild(element_#123) ← 主动将元素从 OldParent 剥离
└─ 元素进入 inactive 状态 ...
└─ _activateWithParent → 绑定到 NewParent
无论哪种顺序,结果一致。Flutter 的设计保证了 reparenting 路径对 build 顺序是鲁棒的。
6. _InactiveElements:非活跃元素的缓冲池
_InactiveElements 是一个内部类,管理所有被 deactivate 但尚未 unmount 的元素:
// packages/flutter/lib/src/widgets/framework.dart
class _InactiveElements {
bool _locked = false;
final Set<Element> _elements = HashSet<Element>();
void add(Element element) {
assert(!_locked);
assert(!_elements.contains(element));
assert(element._parent == null);
switch (element._lifecycleState) {
case _ElementLifecycle.active:
_deactivateRecursively(element); // 递归 deactivate
_elements.add(element);
case _ElementLifecycle.inactive:
_elements.add(element); // 已在 inactive,直接加入
case _ElementLifecycle.initial ||
_ElementLifecycle.failed ||
_ElementLifecycle.defunct:
assert(false); // 不应出现
}
}
void remove(Element element) {
assert(!_locked);
assert(_elements.contains(element));
assert(element._parent == null);
_elements.remove(element);
assert(element._lifecycleState == _ElementLifecycle.inactive);
}
void _unmountAll() {
_locked = true;
final List<Element> elements = _elements.toList()..sort(Element._sort);
_elements.clear();
try {
elements.reversed.forEach(_unmount); // 从深到浅递归 unmount
} finally {
assert(_elements.isEmpty);
_locked = false;
}
}
}
关键设计点:
- 排序后从深到浅 unmount:
_unmountAll先按Element._sort排序,然后reversed从深层开始清理。这是为了确保子节点先于父节点被销毁,符合资源释放的顺序。 _locked锁:在_unmountAll期间防止外部修改集合。这保证了在遍历过程中不会出现并发修改。_deactivateRecursively:add时如果元素还是 active 状态,先递归调用deactivate()。这确保了进入 inactive 集合的元素都处于正确的生命周期状态。
7. 帧末清理:finalizeTree
finalizeTree 由 WidgetsBinding.drawFrame 调用,在 build 和 paint 之间执行(源码位于 packages/flutter/lib/src/widgets/binding.dart):
// packages/flutter/lib/src/widgets/binding.dart
void drawFrame() {
// ...
buildOwner!.buildScope(rootElement!); // ① Build
super.drawFrame(); // ② Layout → Paint (RendererBinding)
buildOwner!.finalizeTree(); // ③ 清理 + 校验
// ...
}
finalizeTree 的实现位于 packages/flutter/lib/src/widgets/framework.dart:
void finalizeTree() {
// ...
try {
lockState(_inactiveElements._unmountAll); // 清理所有 inactive 元素
// 通过 lockState 保证在 unmount 期间不会意外触发 setState
assert(() {
try {
_debugVerifyGlobalKeyReservation(); // 校验 1
_debugVerifyIllFatedPopulation(); // 校验 2
// 校验 3:检查 shenanigans 列表
if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans
?.isNotEmpty ?? false) {
// ... 收集重复 key,抛异常
}
} finally {
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans?.clear();
}
return true;
}());
} catch (e, stack) {
_reportException(
ErrorSummary('while finalizing the widget tree'), e, stack
);
}
}
lockState 包装了 _unmountAll 调用——它的作用是防止在 unmount 过程中触发 setState 等状态变更,保证清理过程的事务性。
7.1 Debug 校验层级
Debug 模式下的三层校验(仅在 Debug 模式生效,release 模式这些字段为 null 并被编译器消除):
第一层:_debugVerifyGlobalKeyReservation
检查是否存在两个不同的父节点同时「预留」了同一个 GlobalKey 的子节点:
void _debugVerifyGlobalKeyReservation() {
assert(() {
final keyToParent = <GlobalKey, Element>{};
_debugGlobalKeyReservations?.forEach(
(Element parent, Map<Element, GlobalKey> childToKey) {
// 跳过已 defunct 或 renderObject 未 attach 的父节点
if (parent._lifecycleState == _ElementLifecycle.defunct ||
parent.renderObject?.attached == false) return;
childToKey.forEach((Element child, GlobalKey key) {
if (child._parent == null) return; // 已脱离
if (keyToParent.containsKey(key) && keyToParent[key] != parent) {
// 同一个 GlobalKey 被两个不同的父节点预留 → 抛异常
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Multiple widgets used the same GlobalKey.'),
// ...
]);
} else {
keyToParent[key] = parent;
}
});
});
_debugGlobalKeyReservations?.clear();
return true;
}());
}
第二层:_debugVerifyIllFatedPopulation
检查 _debugIllFatedElements 中是否有非 defunct 元素。这些元素是在 _registerGlobalKey 时被覆盖的旧持有者:
void _debugVerifyIllFatedPopulation() {
assert(() {
Map<GlobalKey, Set<Element>>? duplicates;
for (final Element element
in _debugIllFatedElements ?? const <Element>{}) {
if (element._lifecycleState != _ElementLifecycle.defunct) {
final key = element.widget.key! as GlobalKey;
assert(_globalKeyRegistry.containsKey(key));
duplicates ??= <GlobalKey, Set<Element>>{};
final Set<Element> elements =
duplicates.putIfAbsent(key, () => <Element>{});
elements.add(element);
elements.add(_globalKeyRegistry[key]!);
}
}
_debugIllFatedElements?.clear();
if (duplicates != null) {
// 抛异常:同一 GlobalKey 被多个 widget 使用
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Multiple widgets used the same GlobalKey.'),
// ...
]);
}
return true;
}());
}
第三层:Shenanigans 检测
检查 _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans 中是否有非 defunct 的父节点。这个列表记录的是「被抢夺了子节点,但在本帧内未 rebuild」的父节点——如果父节点在子节点被抢走后从未 rebuild,它仍然认为自己有那个 GlobalKey 子节点,这意味着存在重复 GlobalKey。
// 在 _retakeInactiveElement 中记录
parent.owner!._debugTrackElementThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans(
parent, key,
);
// 在父节点的 updateChild 中清除
child.owner!._debugElementWasRebuilt(child);
// 在 finalizeTree 中检查
if (_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans
?.isNotEmpty ?? false) {
final Set<GlobalKey> keys = HashSet<GlobalKey>();
for (final Element element
in _debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans!.keys) {
if (element._lifecycleState != _ElementLifecycle.defunct) {
keys.addAll(
_debugElementsThatWillNeedToBeRebuiltDueToGlobalKeyShenanigans![element]!,
);
}
}
if (keys.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Duplicate GlobalKey detected in widget tree.'),
// ... 复杂的错误信息,语法上做了单/复数的动态适配
]);
}
}
8. _ElementLifecycle 枚举:状态机的完整定义
Element 的生命周期由 _ElementLifecycle 枚举决定:
initial → active → inactive → defunct
↓ ↑
failed ──────┘ (不可逆)
- initial:刚创建,从未挂载。
mount()后变为 active。 - active:在树中,参与所有框架操作。通过
deactivate()变为 inactive。 - inactive:脱离树,但保留 GlobalKey 映射。有两个去向:
- 通过
_activateWithParent()→activate()回到 active(reparenting)。 - 通过
unmount()变为 defunct(finalizeTree 清理)。
- 通过
- failed:子树在构建时抛出不可恢复异常。不可逆,调用
deactivate()后跨过 inactive 直接到 failed。 - defunct:已销毁,不可逆。
_widget和_dependencies被清空。
9. 关键设计选择解读
9.1 为什么 GlobalKey 的相等性基于对象标识?
普通的 GlobalKey() 和 LabeledGlobalKey 不重写 operator ==,使用 Dart 默认的 identical 判等。这意味着:
final keyA = GlobalKey(); // 创建新对象
final keyB = GlobalKey(); // 创建另一个新对象
// keyA != keyB (不同的 Dart 对象实例)
这是有意为之。如果 GlobalKey 像 ValueKey 那样按值判等,那么两个在不同位置独立创建的「值为相同字符串」的 GlobalKey 会被认为是同一个,从而导致意外的 reparenting 和重复 key 错误。
正是由于对象标识判等,开发者必须将 GlobalKey 存储为长期对象(如 State 的字段),而不是在 build 方法中重新创建。
9.2 为什么 deactivate 不注销 GlobalKey?
如果 deactivate 时立即注销 GlobalKey,那么当新位置的 _retakeInactiveElement 尝试通过 key._currentElement 查找元素时就会返回 null——因为注册表已清空。框架将不得不创建全新的 Element,整个子树的 State 都会丢失。
保留注册表映射直到 unmount,给了 reparenting 一个「时间窗口」。这个窗口很窄——只持续到当前帧的 finalizeTree——但这刚好足够同帧内的 reparenting 完成。
9.3 为什么 reparenting 必须在同一帧内?
因为 finalizeTree 在每帧结束时无条件调用 _inactiveElements._unmountAll(),销毁所有未被重用的 inactive 元素。如果新位置的 widget 出现在下一帧,这个元素已经被 defunct 了。
这个设计保证了 inactive 集合不会无限增长(内存泄漏),也避免了跨帧的复杂状态同步问题。
9.4 reparenting 的性能代价
注释中明确说明:Reparenting 相对昂贵,因为它会:
- 递归调用
deactivate()和activate()在整个子树上。 - 触发所有依赖
InheritedWidget的子 Widget 重建(didChangeDependencies)。
因此,对于不需要跨树移动的 widget,应该优先使用 ValueKey 或 ObjectKey。
9.5 为什么 GlobalKey 的类型参数是 T extends State<StatefulWidget>?
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key
类型参数不是任意类型,而是约束为 State<StatefulWidget>。这是因为 currentState 的返回值类型就是这个 T:
T? get currentState => switch (_currentElement) {
StatefulElement(:final T state) => state,
_ => null,
};
对于非 StatefulWidget,虽然 GlobalKey 仍然可用(currentContext 和 currentWidget 仍然有效),但 currentState 总是返回 null。
10. 边界情况与常见误区
10.1 在 build 方法中创建 GlobalKey
// ❌ 错误:每次 rebuild 创建新 GlobalKey
@override
Widget build(BuildContext context) {
final myKey = GlobalKey(); // 每次都不同!
return Container(key: myKey);
}
每次 build 调用都会创建一个全新的 Dart 对象实例。新 key 与旧 key 不相等(Dart 对象标识),因此框架会 deactivate 旧元素并创建新元素。这导致:
- State 全部丢失。
- GestureDetector 等无法追踪持续的手势。
正确做法:将 GlobalKey 存储在 State 中:
class MyWidget extends StatefulWidget { ... }
class _MyWidgetState extends State<MyWidget> {
final myKey = GlobalKey(); // ✅ 长期持有
@override
Widget build(BuildContext context) {
return Container(key: myKey);
}
}
10.2 两棵树同时使用同一个 GlobalKey
// ❌ 错误
Column(
children: [
Container(key: myGlobalKey), // 第一次使用
Container(key: myGlobalKey), // 同一父节点 → 抛异常
],
)
同一父节点下的重复 GlobalKey 会在 _retakeInactiveElement 中直接抛出 FlutterError。不同父节点下的重复 GlobalKey 也会在 finalizeTree 的 debug 校验中被检测到。
10.3 Widget 类型变更导致 reparenting 失败
如果新旧 Widget 的 runtimeType 不同,Widget.canUpdate 返回 false,_retakeInactiveElement 返回 null。旧元素被 unmount(连同它的 State),新元素从头创建。
例如:TextField → TextFormField,虽然都是输入框,但 runtimeType 不同,reparenting 会失败。
10.4 GlobalObjectKey 与命名空间隔离
GlobalObjectKey 的 operator == 包含了 runtimeType 检查:
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is GlobalObjectKey<T> && identical(other.value, value);
}
为了避免不同来源的 GlobalObjectKey 意外冲突,建议创建私有子类:
class _MyKey extends GlobalObjectKey<MyWidgetState> {
const _MyKey(super.value);
}
10.5 跨帧移动不会触发 reparenting
如果你在一个帧中移除 widget,在下一帧的另一个位置重新添加,reparenting 不会发生:
帧 N :OldParent rebuild → widget with GlobalKey removed → element deactivated
帧 N :finalizeTree → element unmounted(GlobalKey 注销)
帧 N+1:NewParent rebuild → widget with same GlobalKey → _retakeInactiveElement 返回 null
→ 创建全新 Element(State 丢失!)
11. 总结
GlobalKey 的实现可以浓缩为三个核心机制:
- 全局注册表:
BuildOwner._globalKeyRegistry(Map<GlobalKey, Element>),以对象标识为键,提供全应用范围的 Element 查找。 - 注册/注销时机:注册发生在
Element.mount,注销发生在Element.unmount。deactivate不注销——这是 reparenting 得以工作的前提。 - Reparenting 流水线:
inflateWidget→_retakeInactiveElement(抢夺)→_activateWithParent(激活)→updateChild(同步)。整个过程在同一帧内完成。