likes
comments
collection
share

AutomaticKeepAlive详解

作者站长头像
站长
· 阅读数 17

前言

开发过Flutter app的同学们应该都知道,在使用ListView或者TabBarView的时候,如果不做处理,滑动过程中列表项的状态可能会丢失,比如,当前某个列表项是选中的状态,把它滑出去再滑回来,选中状态就不见了,列表项还原成未选中的状态。要修复这个问题,我们只需要在列表项WidgetState中混入AutomaticKeepAliveClientMixin,再覆写wantKeepAlive返回true,最后还要在build()内调用super.build(context)。这样处理以后,列表项的状态不管怎么滑都会一直保留了。

// 混入`AutomaticKeepAliveClientMixin`
class _MyListItemState extends State<MyListItem> with AutomaticKeepAliveClientMixin{
  @override
  Widget build(BuildContext context) {
  //调用`super.build(context)`
    super.build(context);
    return ListTile(
      title: Text('Item ${widget.index + 1}'),
    );
  }

  @override
  // 覆写`wantKeepAlive`返回`true`
  bool get wantKeepAlive => true;
}

这种处理看起来也并不复杂,但我们还是会有一些疑问,为什么这样做了状态就会保留?背后发生了一些什么事情?为什么要覆写wantKeepAlive?我混入AutomaticKeepAliveClientMixin不就告诉你要保留状态嘛,何必多此一举?还有那个调用super.build(context)看起来又是多此一举而且容易被忽略而导致出错。

要回答上面这些问题,我们就不得不深入源码来探究AutomaticKeepAlive工作原理了。

源码

为方便理解,本文中直接用“保活”来指代"keep alive"。先来看看AutomaticKeepAliveClientMixin的源码。

AutomaticKeepAliveClientMixin

mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {
  KeepAliveHandle? _keepAliveHandle;

  void _ensureKeepAlive() {
    _keepAliveHandle = KeepAliveHandle();
    KeepAliveNotification(_keepAliveHandle!).dispatch(context);
  }

  void _releaseKeepAlive() {
    _keepAliveHandle!.release();
    _keepAliveHandle = null;
  }

  @protected
  bool get wantKeepAlive;

  
  @protected
  void updateKeepAlive() {
    if (wantKeepAlive) {
      if (_keepAliveHandle == null)
        _ensureKeepAlive();
    } else {
      if (_keepAliveHandle != null)
        _releaseKeepAlive();
    }
  }

  @override
  void initState() {
    super.initState();
    if (wantKeepAlive)
      _ensureKeepAlive();
  }

  @override
  void deactivate() {
    if (_keepAliveHandle != null)
      _releaseKeepAlive();
    super.deactivate();
  }

  @mustCallSuper
  @override
  Widget build(BuildContext context) {
    if (wantKeepAlive && _keepAliveHandle == null)
      _ensureKeepAlive();
    return const _NullWidget();
  }
}

源码整体并不复杂,当wantKeepAlivetrue的时候在initStatebuild的时候都调用_ensureKeepAlivedeactivate的时候调用_releaseKeepAlive。从函数名字就可以看出这两个一个是说state要保活,另一个是说不需要保活了。所以我们关注这两个函数就行了。从_ensureKeepAlive的函数体内我们又遇到两个没见过的类KeepAliveNotificationKeepAliveHandle。但我们能发现设置与取消保活都是这两个类在做事。设置的时候KeepAliveNotification的调用,参数里带了一个KeepAliveHandle。而取消的时候只是调用了一下_keepAliveHandle!.release(),和KeepAliveNotification就没什么关系了。

感觉好像就是用KeepAliveNotification在哪里注册了一个handle,取消的时候通过这个handle取消就完事了。

class KeepAliveNotification extends Notification {
  
  final Listenable handle;
}

KeepAliveNotification继承自Notification,那它的作用显然就是往它的某个祖先节点发送通知的,而且可以断定的是这个祖先节点的类型就是NotificationListener<KeepAliveNotification>了。

class KeepAliveHandle extends ChangeNotifier {
  void release() {
    notifyListeners();
  }
}

KeepAliveHandle则继承自我们熟悉的ChangeNotifier。可见在AutomaticKeepAliveClientMixin中我们就能看到它利用了两种在Flutter中非常常见的节点间通讯方式。

用图来表示就是这个样子:

AutomaticKeepAlive详解

所以接下来的问题就是要找到接收方,也就是NotificationListener<KeepAliveNotification>看看发生了什么。

AutomaticKeepAlive

那么上哪里去找NotificationListener<KeepAliveNotification>呢?其实远在天边,尽在眼前。AutomaticKeepAliveClientMixin所在的同一个dart文件里。

  void _updateChild() {
    _child = NotificationListener<KeepAliveNotification>(
      onNotification: _addClient,
      child: widget.child!,
    );
  }

上面这个函数_updateChild()里我们就能看到NotificationListener<KeepAliveNotification>了,而这个函数属于_AutomaticKeepAliveState,这是个State,自然的也就有对应的StatefulWidget,那就是AutomaticKeepAlive。显然的我们需要关注的是_AutomaticKeepAliveState的实现。这里我们只看一些关键的源码,从上面的函数体可以明显看到收到NotificationListener<KeepAliveNotification>通知消息的处理函数是_addClient。我们就来看看这个通知消息是怎么被处理的。

bool _addClient(KeepAliveNotification notification) {
    final Listenable handle = notification.handle;
    _handles ??= <Listenable, VoidCallback>{};
    
    _handles![handle] = _createCallback(handle);
    handle.addListener(_handles![handle]!);
    if (!_keepingAlive) {
      _keepingAlive = true;
      final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
      if (childElement != null) {
        _updateParentDataOfChild(childElement);
      } else {
        
        SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
          if (!mounted) {
            return;
          }
          final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
          assert(childElement != null);
          _updateParentDataOfChild(childElement!);
        });
      }
    }
    return false;
  }

函数体比较长,但是总结起来就做了两件事,首先是把带过来的handle给包裹成VoidCallback,再把这个回调设置成自己的一个listener,放进一个Map里存起来。然后就是去找ParentDataElement<KeepAliveParentDataMixin>类型的孩子节点。找到了调用_updateParentDataOfChild

所以我们能发现设置保活通知到这里会变成更新某个孩子节点的ParentData。那么取消保活的调用呢?显然就要去看那个回调做了什么。

VoidCallback _createCallback(Listenable handle) {
    return () {
      _handles!.remove(handle);
      if (_handles!.isEmpty) {
        if (SchedulerBinding.instance!.schedulerPhase.index < SchedulerPhase.persistentCallbacks.index) {          
          setState(() { _keepingAlive = false; });
        } else {       
          _keepingAlive = false;
          scheduleMicrotask(() {
            if (mounted && _handles!.isEmpty) {
              setState(() {
              });
            }
          });
        }
      }
    };
  }

也是两件事,把自己从那个Map里移除,如果Map为空了,会把_keepingAlive置为false,再触发一下重新构建过程。

然后最后需要关注的就是build方法了。

@override
  Widget build(BuildContext context) {
    
    return KeepAlive(
      keepAlive: _keepingAlive,
      child: _child!,
    );
  }

返回的是个KeepAlive

KeepAlive

这个Widget继承自ParentDataWidget<KeepAliveParentDataMixin>。可见我们上面说的保活通知的处理是设置ParentData,那其实就是着落到这里了。我们只要关注其applyParentData实现就好了:

void applyParentData(RenderObject renderObject) {
    assert(renderObject.parentData is KeepAliveParentDataMixin);
    final KeepAliveParentDataMixin parentData = renderObject.parentData! as KeepAliveParentDataMixin;
    if (parentData.keepAlive != keepAlive) {
      parentData.keepAlive = keepAlive;
      final AbstractNode? targetParent = renderObject.parent;
      if (targetParent is RenderObject && !keepAlive)
        targetParent.markNeedsLayout(); // No need to redo layout if it became true.
    }
  }

其作用呢就是找到孩子RenderObject,然后设置keepAlive

至此整个设置保活的过程就完整了。总结起来就是从设置保活的出发点State,绕了一大圈最后落脚点是列表项下的某个RenderObject被设置了ParentData。用图来表示就是下面这个样子:

AutomaticKeepAlive详解

那么问题来了,我们保活的目的是保证状态State的生存,可为啥搞了半天却是给RenderObject设了个标志位。它们之间又有什么关系呢?为理解这个问题,就不得不提到Sliver了。Flutter中涉及高效滚动的地方基本上都会用Sliver子框架。包括本文保活所需要的ListViewTabBarView。Sliver自身是比较复杂的,要总体介绍一下可以另外再写几篇文章了,需要的话这里大家可以参考其他作者的关于Sliver的文章。本文因为关注的是状态保活,所以只就涉及保活的Sliver缓存方面做一下说明。

RenderSliverMultiBoxAdaptor

Sliver对保活的处理是在类RenderSliverMultiBoxAdaptor中的两个函数里,而这个类看名字我们就知道这是RenderObject的子类,所以保活还是不保活就看它怎么处理自己的孩子RenderObject喽。涉及的这两个函数就是_createOrObtainChild_destroyOrCacheChild了:

  void _destroyOrCacheChild(RenderBox child) {
    final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
    if (childParentData.keepAlive) {
      remove(child);
      _keepAliveBucket[childParentData.index!] = child;
      child.parentData = childParentData;
      super.adoptChild(child);
      childParentData._keptAlive = true;
    } else {
      _childManager.removeChild(child);
    }
  }

先来看_destroyOrCacheChild,这个函数在列表项被彻底划出以后需要回收的时候被调用。此时这个RenderObject要么要被彻底销毁,要么就缓存起来下次再用。而决定是销毁还是缓存起来的正是我们之前设定的那个keepAlive标志。当childParentData.keepAlivetrue的时候,该孩子RenderObject就被保存在_keepAliveBucket数组中了。否则就调用_childManager.removeChild将其销毁。这里大家可能会有疑问,明明缓存的是个RenderObject,怎么会把State给保留下来了呢?原因是缓存分支内没有调用_childManager.removeChild。这个_childManager其实就是SliverMultiBoxAdaptorElement,看名字我们就知道它是RenderSliverMultiBoxAdaptor对应的element。所以_childManager.removeChild没有被调用的话,这个孩子RenderObject对应的Element也就依然活在SliverMultiBoxAdaptorElement中,Element活着,那其子树里的State自然也都活着。保活的目的也就达到了。

缓存了那就会有再取出来复用的时候,复用就是通过_createOrObtainChild调用

void _createOrObtainChild(int index, { required RenderBox? after }) {
    invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
    
      if (_keepAliveBucket.containsKey(index)) {
        final RenderBox child = _keepAliveBucket.remove(index)!;
        final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
        
        dropChild(child);
        child.parentData = childParentData;
        insert(child, after: after);
        childParentData._keptAlive = false;
      } else {
        _childManager.createChild(index, after: after);
      }
    });
  }

逻辑就是如果在_keepAliveBucket数组里有对应的RenderObject就直接拿出来用,否则的话就让_childManager去根据当前索引去新建孩子节点,这里新建我们列表项的element子树和render object子树。

AutomaticKeepAliveClientMixin的一些思考

通过上面的源码分析,我们对AutomaticKeepAliveClientMixin的作用机制就有了一定的了解。那么,在使用AutomaticKeepAliveClientMixin的时候有一些细节就需要我们着重考虑了。

  • 当需要保存状态的时候我是否只需要无脑设置wantKeepAlivetrue就完事了?

讨论这个问题的时候我们就要想到状态保活的代价是什么,而代价显然就是在显示范围之外的时候,保活的列表项对应的element子树和widget子树都是一直留存在内存中的,也就是占用更多的内存空间,如果我们的列表里有成千上万条数据,此时还是无脑设置wantKeepAlivetrue的话那它们就都会赖在内存里不走了。如此一来,第一内存有可能爆掉,第二就是我还费事用sliver干啥,直接用SingleChildScrollView不就结了吗,区别就是滑动过程中爆掉还是一开始就爆掉。

所以,正确的理念应该是只有真正需要保留状态的时候才将wantKeepAlive置为true,不需要的时候将其置为false,以便回收。需要保留状态的列表项越少越好。

举个例子,比如我有个页面是城市选择列表,而这是个单选的列表,我选了北京就不能再选择上海,那么如果这个列表需要保留选择状态最好不要无脑设置wantKeepAlivetrue,否则几百个市县有可能都会住在内存里不走了。此时更好的办法是只保留那一个选中城市的状态,其他的都默认wantKeepAlivefalse。这样的话缓存起来的就只有一条了。所以Flutter也贴心的在AutomaticKeepAliveClientMixin给我们提供了个函数updateKeepAlive,以便在需要的时候更新保活状态:

@protected
  void updateKeepAlive() {
    if (wantKeepAlive) {
      if (_keepAliveHandle == null)
        _ensureKeepAlive();
    } else {
      if (_keepAliveHandle != null)
        _releaseKeepAlive();
    }
  }

这个列表的例子我们只需要在选择状态发生变化的时候调用一下updateKeepAlive更新保活状态。同时也不要再写死wantKeepAlivetrue,需要改成类似下面的样子:

@override
  // _selected表示选中状态,true为选中,false为未选中
  bool get wantKeepAlive => _selected;

上面是个单选的例子,那如果我的列表是个多选的列表呢?比如邮件列表,我有可能要全选然后再做删除等等操作呢?

多选这种情况我们就要综合考虑了,如果是有限的多选,比如一次最多5条10条,那么可以参照上述单选的例子来处理,但是如果是不做限制的话,那可能这种动态设置的方式也没有减少内存占用,此时我们可以考虑将状态提升,即把选择状态提升到列表的父节点那里去,或者可以使用Provider等状态管理工具来做提升。列表项自身不保存状态,这种情况下我们都不需要混入AutomaticKeepAliveClientMixin,或者更进一步,就是完全使用StatelessWidget而不用StatefulWidget

  • 当你设置了wantKeepAlivetrue/false就一定会保留/不保留状态吗?

如果你的列表项里只有一个State混入了AutomaticKeepAliveClientMixin,那么这个State就决定了是否保留状态,但是如果你的列表项是个复杂Widget,里面有不止一个State混入了AutomaticKeepAliveClientMixin,那么每个State调用_ensureKeepAlive后会在_handles里留下一个记录。而每次调用_releaseKeepAlive则会从_handles里去除对应的记录,但是这里要注意一点,就是只有在_handles为空以后才会将_keepingAlive置为false。换句话说,在有多个State的情况下,只有所有State都声明不保留状态的情况下,才会将其回收,所以结论就是设置了wantKeepAlivetrue就一定会保留状态,设置wantKeepAlivefalse不一定就不保留状态,这得看看有没有其他State仍然在要求保留。

另外还有一个要注意的点就是当存在sliver嵌套的情况下,设置保留状态只会对离自己最近的那个Sliver生效。比如你在TabBarView里嵌套了个ListView。然后在列表项里声明要保留状态,那么这个声明只会在ListView里生效。如果切到别的tab然后再切回来的话,状态也就丢失了。

总结

本文通过源码对AutomaticKeepAlive做了详尽的介绍,相信看过以后大家对sliver中状态保留的原理有了一定的了解。然后又对于我们在使用AutomaticKeepAliveClientMixin的时候可能会遇到的一些问题做了相应的分析,希望能对于使用状态保留的正确姿势有所帮助。

(全文完)