Flutter原理篇:key原理解析

今天做了一个类似抖音的app的时候,当点赞删除的时候,总是出现多个item重复出现的情况,于是才发现key的作用如此之大。所以今天给大家讲讲key的原理到底是什么。

key的使用场景只有一个,当你想给一系列相同类型的并有各种不同状态的widget进行添加,删除,排序的时候会使用到

一、Key的分类

Key是所有key的父类。且主要有两类key,即GlobalKeyLocalKey

二、key的使用场景

GlobalKey(全局key)
  •  主要用于对widget的更新(局部更新),获取widget的大小,获取widget父widget的属性,获取widget的element       

LocalKey(局部Key)
  • UniqueKey主要用于动画的刷新。或用于需要每次都要重建刷新widget的场景
  • ObjectKey(value)传入的是对象的引用,通过比较当前位置对象引用或类型是否相同进行更新,一般用于位置调换。或列表更新。
  • ValueKey(value)传入的是值,通过比较该值是否相等或类型是否相同进行判断更新,同ObjectKey,主要区别是比较的值不同

三、GlobalKey使用原理

class GlobalKey {
    //从注册表中获取到绑定到element
    Element? get _currentElement =>                 WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];

    //获取context即element
    BuildContext? get currentContext => _currentElement;

    //获取绑定到widget
    Widget? get currentWidget => _currentElement?.widget;

    //获取statefulWidget的state状态
    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;
      }
}


//由BuildOwner持有全局的globalkey对应的element
class BuildOwner {
    final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};
    void _registerGlobalKey(GlobalKey key, Element element) {
        //注册globalkey
        _globalKeyRegistry[key] = element;
    }
    void _unregisterGlobalKey(GlobalKey key, Element element) {
        //解除globalkey
        if (_globalKeyRegistry[key] == element) {
          _globalKeyRegistry.remove(key);
        }
    }
}
//element,element第一次挂载的时候对globalkey的第一次注册
class Element {
    //第一次挂载element时注册globalkey
    void mount(Element? parent, Object? newSlot) {
        final Key? key = widget.key;
        if (key is GlobalKey) {
          owner!._registerGlobalKey(key, this);
        }
    }
    //element卸载时同时移除globalkey
    void unmount(Element? parent, Object? newSlot) {
        final Key? key = widget.key;
        if (key is GlobalKey) {
          owner!._unregisterGlobalKey(key, this);
        }
    }
}
GlobalKey流程
  • widget在第一次挂载mount的时候会使用全局的owner渲染通道对象将key对应的element进行保存
  • widget在卸载unmount的时候再使用owner对象将key对应的element进行移除操作
  • widget对应的element保存在全局BuildOwner_globalKeyRegistry中。


//新widget的第一次加载
Element inflateWidget(Widget newWidget, Object? newSlot) {
    try {
      final Key? key = newWidget.key;
      //判断如果是GlobalKey则尝试获取对应的element
      if (key is GlobalKey) {
        //获取globalkey对应的element
        final Element? newChild = _retakeInactiveElement(key, newWidget);
        if (newChild != null) {
          try {
            newChild._activateWithParent(this, newSlot);
          } catch (_) {
            try {
              deactivateChild(newChild);
            } catch (_) {
              // Clean-up failed. Only surface original exception.
            }
            rethrow;
          }
          //如果element存在,则基于该element进行下一步的更新
          final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
          return updatedChild!;
        }
      }
      //如果通过globalKey获取的element不存在则进行element的创建,继续往下走
      final Element newChild = newWidget.createElement();
      newChild.mount(this, newSlot);
      return newChild;
    } finally {

    }
  }

Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
    //从BuildOwner的注册表中取出element
    final Element? element = key._currentElement;
    if (element == null) {
      return null;
    }
    //一般key相同类型即是相同
    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;
  }
void deactivateChild(Element child) {
    child._parent = null;
    child.detachRenderObject();
    owner!._inactiveElements.add(child); 
  }
void _activateWithParent(Element parent, Object? newSlot) {
    _parent = parent;
    _updateDepth(_parent!.depth);
    _activateRecursively(this);
    attachRenderObject(newSlot);
  }
static void _activateRecursively(Element element) {
   element.activate();
   element.visitChildren(_activateRecursively);
}
GlobalKey使用流程
  • 在widget的第一次加载的时候调用了inflateWidget,并通过_retakeInactiveElement获取注册表中GlobalKey对应的element。
  • 如果获取的element为空则新建一个element,并进行element的挂载
  • 如果element不为空则调用_activateWithParent先对父element进行绑定持有,并通过_updateDepth更新父element的depth值,方便后续更新使用。
  • 再调用自身element的_activateRecursively方法递归调用activate。
void activate() {
    assert(_lifecycleState == _ElementLifecycle.inactive);
    assert(owner != null);
    final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
    _lifecycleState = _ElementLifecycle.active;
    //清空当前element持有的依赖列表
    _dependencies?.clear();
    _hadUnsatisfiedDependencies = false;
    //重新从父element中获取新的依赖列表
    _updateInheritance();
    attachNotificationTree();
    if (_dirty) {
      owner!.scheduleBuildFor(this);
    }
    //如果有依赖项,则执行相关的state的生命周期函数didChangeDependencies()
    if (hadDependencies) {
      didChangeDependencies();
    }
  }
///将element加入到_dirtyElements脏列表,等待vsync请求到来的时候进行更新相关element
  void scheduleBuildFor(Element element) {
}
//注意这里,递归调用每个子element的_activateRecursively,即_activateRecursively中的activate方法
@override
  void visitChildren(ElementVisitor visitor) {
    if (_child != null) {
      visitor(_child!);
    }
  }

activate:主要是处理依赖关系,更新依赖列表,并调用didChangeDependencies的生命周期函数(如果有依赖)

visitChildren:递归调用子element的activate方法更新依赖列表等。

四、LocalKey使用原理

localKey是局部key,作用于该节点下的各个子节点,所以一般与MultiChildRenderObjectElement这个element有关

所以我们先从MultiChildRenderObjectElement的update更新子element开始分析

void update(MultiChildRenderObjectWidget newWidget) {
    super.update(newWidget);
    final MultiChildRenderObjectWidget multiChildRenderObjectWidget = widget as MultiChildRenderObjectWidget;
    //这里即将开始子节点的更新逻辑。
    _children = updateChildren(_children, multiChildRenderObjectWidget.children, forgottenChildren: _forgottenChildren);
    //这里的_forgottenChildren存储的就是使用GlobalKey的element。清除GlobalKey相关的element
    _forgottenChildren.clear();
  }

List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element>? forgottenChildren, List<Object?>? slots }) {
    Element? replaceWithNullIfForgotten(Element child) {
      return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;
    }
    //头部索引
    int newChildrenTop = 0;
    int oldChildrenTop = 0;
    //尾部索引
    int newChildrenBottom = newWidgets.length - 1;
    int oldChildrenBottom = oldChildren.length - 1;

    //创建一个存储新列表的集合
    final List<Element> newChildren = List<Element>.filled(newWidgets.length, _NullElement.instance);

    Element? previousChild;
    return newChildren;
}

首先我们简单梳理一下,这里开始element的更新,主要分为三个阶段进行更新,更新前会先创建索引,通过索引获取对应的widget

  • 这里会先定义一个过滤带有globalKey的方法replaceWithNullIfForgotten,后面会用到
  • 创建两个头部索引以及尾部索引,方便后期进行遍历更新

遍历更新会分为三个阶段

第一阶段
//第一阶段先对头部索引进行遍历
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
      //先清除带有globalkey的索引,因为globalkey不在更新范围内
      final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
      final Widget newWidget = newWidgets[newChildrenTop];
      //只对非活跃状态的element进行更新
      assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
      //直到遇到不可更新的element就退出循环
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {
        break;
      }
      //如果可更新则进行更新操作
      final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
      assert(newChild._lifecycleState == _ElementLifecycle.active);
      //将更新后的element进行保存
      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];
      assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {
        break;
      }
      oldChildrenBottom -= 1;
      newChildrenBottom -= 1;
}
流程梳理
  • 开始会先前后对满足Widget.canUpdate(oldChild.widget, newWidget)的widget进行更新,并存入newChildren列表中
  • 同时更新索引值继续遍历,直到遇到不可更新的节点停止遍历(即类型不同或key不同)
  • 头部索引遍历完继续遍历尾部索引,注意这里尾部索引不做任何更新,只是更新索引值,即记录不满足更新条件Widget.canUpdate的索引值。
第二阶段

final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
    Map<Key, Element>? oldKeyedChildren;
    if (haveOldChildren) {
      oldKeyedChildren = <Key, Element>{};
      while (oldChildrenTop <= oldChildrenBottom) {
        //此时先过滤带有globalkey的element
        final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
        assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
        if (oldChild != null) {
          if (oldChild.widget.key != null) {
            //保存带有key的element
            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) {
          //通过新widget的key从oldKeyedChildren中获取对应的element
          oldChild = oldKeyedChildren![key];
          if (oldChild != null) {
            //如果有element则判断类型是否相同
            if (Widget.canUpdate(oldChild.widget, newWidget)) {
              //移除相关的element
              oldKeyedChildren.remove(key);
            } else {
             
              oldChild = null;
            }
          }
        }
      }
      assert(oldChild == null || Widget.canUpdate(oldChild.widget, newWidget));
      //如果有相同的key则进行更新,计算新的element节点
      final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
      //将创建的新的element节点存入newChildren列表
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
    }
流程梳理
  • 第一步通过oldKeyedChildren记录原来旧的带有key(非globalkey)的element节点以备后续使用
  • 第二步开始遍历剩余节点,判断原来的节点与新的widget的节点是否相同。如果相同则晴空万里oldKeyedChildren对应的element节点,基于新的widget进行更新即可,复用原来的element。并将element存入newChildren
第三阶段
    //重新获取旧element和新widget的尾部索引,即重置尾部索引
    newChildrenBottom = newWidgets.length - 1;
    oldChildrenBottom = oldChildren.length - 1;

    //从尾部开始更新,开始更新newChildren的尾部
    while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
      final Element oldChild = oldChildren[oldChildrenTop];
      assert(replaceWithNullIfForgotten(oldChild) != null);
      assert(oldChild._lifecycleState == _ElementLifecycle.active);
      final Widget newWidget = newWidgets[newChildrenTop];
      assert(Widget.canUpdate(oldChild.widget, newWidget));
      final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
      assert(newChild._lifecycleState == _ElementLifecycle.active);
      assert(oldChild == newChild || oldChild._lifecycleState != _ElementLifecycle.active);
      newChildren[newChildrenTop] = newChild;
      previousChild = newChild;
      newChildrenTop += 1;
      oldChildrenTop += 1;
    }

    //开始清空未命中使用的key元素
    if (haveOldChildren && oldKeyedChildren!.isNotEmpty) {
      for (final Element oldChild in oldKeyedChildren.values) {
        if (forgottenChildren == null || !forgottenChildren.contains(oldChild)) {
          deactivateChild(oldChild);
        }
      }
    }
流程梳理
  • 从第一阶段和第二阶段遍历完之后,开始遍历剩余节点,则重新计算尾部索引,即重置索引开始第三阶段的遍历
  • 从尾部开始更新,更新newChildren的尾部。,更新完之后开始清空未命中使用的key元素,并调用deactivateChild清空
  • 32
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超盘守

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值