Flutter - Key原理

1,067 阅读4分钟

接触过Flutter的同学肯定对Key并不陌生,我们在所有Widget中都可以看到构造函数中有一个命名参数key,比如在StatefulWidget中:

abstract class StatefulWidget extends Widget{
/// Initializes [key] for subclasses.
 const StatefulWidget({ Key? key }) : super(key: key);
 ...
}

那么我相信你肯定会问这个key是用来做什么的?为何要放一个看似无用的参数在这里呢?今天我们就来一探究竟。

下面是一个🌰

接下来我们仔细看看下面这个例子。关门,放代码😏

// 创建一个页面,中间放三个色块,每点击一次按钮,删除_containers数组中第一个元素
// 请仔细观察色块的变化
class _MyHomePageState extends State<MyHomePage> {

  final List<AAContainer> _containers = [
    const AAContainer('A'),
    const AAContainer('B'),
    const AAContainer('C'),
  ];

  void _updateColor(){

    _containers.removeAt(0);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text('key demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: _containers
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateColor,
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

AAContainer代码如下:

class AAContainer extends StatefulWidget {

  final String title;
  const AAContainer(this.title,{Key? key}) : super(key: key);
  @override
  _AAContainerState createState() => _AAContainerState();
}

class _AAContainerState extends State<AAContainer> {

  final Color _color = Color.fromRGBO(Random().nextInt(256),Random().nextInt(256),Random().nextInt(256),1);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      width: 100,
      color: _color,
      child: Center(
        child: Text(widget.title),
      ),
    );
  }
}

运行结果如下:

16324515098583.jpg

等等,先让我们对这几个颜色的认知达成一致:

  • A色块:红色
  • B色块:蓝色
  • C色块:紫色

我们对上面代码的预想结果应该是 点击第一下按钮的时候红色色块消失, 点击第二下按钮蓝色色块消失, 点击第三下按钮紫色色块消失, 如果是这样,那就太完美了,但是真的是这样的吗 ?请看下面的gif

2021-09-24 10.57.38.gif

诶?咋回事。。。怎么不按套路来呢?要搞清楚这个问题,我们就需要去研究一下Flutter是如何实现页面刷新的

页面刷新原理

总所周知,Flutter采用增量更新的方式来刷新页面,那么它是如何判断那部分发生变化的呢?

setState

我们知道要让StatefulWidget刷新,我们需要调用setState方法,那么这个方法中做了什么,接下来我们从这个切入点来突破。 setState源码

@protected
  void setState(VoidCallback fn) {
    //无关代码已略去 
    _element!.markNeedsBuild();
  }

setState中只调用了markNeedsBuild这一个方法,我们进入到markNeedsBuild

markNeedsBuild
void markNeedsBuild() {
    //无关代码已略去
    if (_lifecycleState != _ElementLifecycle.active)
      return;
    if (dirty)
      return;
    _dirty = true;
    // owner是BuildOwner的实例对象,this是当前element
    owner!.scheduleBuildFor(this);
  }

markNeedsBuild先将当前element标记为dirty,然后将this(当前element)加入到渲染队列中,接下来我们康康scheduleBuildFor这个方法:

scheduleBuildFor
void scheduleBuildFor(Element element) {
    //断言代码已略去
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled!();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
  }

这个方法只是将当前element加入到渲染队列中,并且标记element已经在队列中。这个时候我们就懵逼了,难道到这里就结束了吗 ?通过查看源码我们发现在BuildOwner还有另外一个方法buildScope,通过调试我们发现每次调用setState都会来到这里,我们再来康康这部分的源码:

buildScope
void buildScope(Element context, [ VoidCallback? callback ]) {

  _dirtyElements.sort(Element._sort);
  _dirtyElementsNeedsResorting = false;
  int dirtyCount = _dirtyElements.length;
  int index = 0;
  while (index < dirtyCount) {
      _dirtyElements[index].rebuild();
  }
}

我们发现这段代码是将scheduleBuildFor中加入到_dirtyElements中的element取出来并调用rebuild方法,我们再来查看rebuild源码:

rebuild
  void rebuild() {
    //断言代码已略去
    performRebuild();
  }

StatefulWidget所对应的elementStatefulElementStatefulElement又继承自ComponentElement所以我们直接查看ComponentElementperformRebuild方法:

performRebuild
void performRebuild() {
    // 略去了很多判断代码
    built = build();
    _child = updateChild(_child, built, slot);
}

😤这样找下去是不是很累? 哈哈,别急,马上就结束了。接下来我们来到updateChild方法:

updateChild
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    //如果在widgetTree中当前widget被删除则直接结束,并在ElementTree中也删除它
    if (newWidget == null) {
        deactivateChild(child);
        return;
    }
    ...
    Element newChild;
    if (child != null) {
        ...
        if (hasSameSuperclass && child.widget == newWidget) {
        //如果相同则不进行updata操作
             newChild = child;
        } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        //如果可以更新则进行update操作
             child.update(newWidget);
             newChild = child;
        }else{
            newChild = inflateWidget(newWidget, newSlot);
        }
    }
  }

通过源码我们可以看出,再调用update方法之前,会通过canUpdate来判断是否可以更新,那么我们来看看canUpdate如何实现:

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

这段代码我相信都看的懂,上面的demo中的AAContainerruntimeType是一致的,key也是一致的,所以会调用elementupdate方法,但是在update方法中仅仅只是将当前element的widget指向了新的widget,也就是说把红色element指向了蓝色的widget

update
void update(covariant Widget newWidget) {
    _widget = newWidget;
}

也就是说更新后,红色element还是在element树中,但是红色element.widget却指向了蓝色widget,才会导致上面demo中的错误。

那么这个问题怎么解决呢?

解决方案

我们从上面的分析得出因为调用了update方法导致结果出错,那么我们有没有什么方法能让element不去调用update呢?肯定是有的,那就是我们今天讨论的主题Key。我们对🌰做个改造:

final List<AAContainer> _containers = [
    const AAContainer('A',key: ValueKey('A'),),
    const AAContainer('B',key: ValueKey('B'),),
    const AAContainer('C',key: ValueKey('C'),),
];

只是在创建AAContainer的时候加上了参数key,我们看看结果:

2021-09-24 17.11.01.gif

怎么样,是不是完美了😁😁😁

Key的分类

我们已经知道了key是如何工作的,接下来给大家介绍一下Key的分类:

LocalKey

LocalKeyDiff算法的核心所在,用作Element和Widget进行比较。 以下几种Key也继承自LocalKey:

  • ValueKey: 以任何类型作为key
  • ObjectKey: 以Object对象作为Key
  • UniqueKey: 保证了Key的唯一性,一旦使用UniqueKey,那么将不存在element复用
GlobalKey

GlobalKey可以获取到对应Widget的State对象,我们通过一个🌰 来了解GlobalKey的用法:

class GlobalKeyDemo extends StatelessWidget {
  final GlobalKey<_ChildPageState> _globalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GlobalKeyDemo'),
      ),
      body: ChildPage(
        key: _globalKey,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          _globalKey.currentState.data =
              'old:' + _globalKey.currentState.count.toString();
          _globalKey.currentState.count++;

          _globalKey.currentState.setState(() {});
        },
      ),
    );
  }
}

class ChildPage extends StatefulWidget {
  ChildPage({Key key}) : super(key: key);
  @override
  _ChildPageState createState() => _ChildPageState();
}

class _ChildPageState extends State<ChildPage> {
  int count = 0;
  String data = 'hello';
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: <Widget>[
          Text(count.toString()),
          Text(data),
        ],
      ),
    );
  }
}

上面的🌰 中我们在父widget中更新了子widget的count值,你就说秒不妙吧!

再见,结束!