AutomaticKeepAlive详解
前言
开发过Flutter app的同学们应该都知道,在使用ListView
或者TabBarView
的时候,如果不做处理,滑动过程中列表项的状态可能会丢失,比如,当前某个列表项是选中的状态,把它滑出去再滑回来,选中状态就不见了,列表项还原成未选中的状态。要修复这个问题,我们只需要在列表项Widget
的State
中混入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();
}
}
源码整体并不复杂,当wantKeepAlive
为true
的时候在initState
和build
的时候都调用_ensureKeepAlive
。deactivate
的时候调用_releaseKeepAlive
。从函数名字就可以看出这两个一个是说state
要保活,另一个是说不需要保活了。所以我们关注这两个函数就行了。从_ensureKeepAlive
的函数体内我们又遇到两个没见过的类KeepAliveNotification
和KeepAliveHandle
。但我们能发现设置与取消保活都是这两个类在做事。设置的时候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中非常常见的节点间通讯方式。
用图来表示就是这个样子:
所以接下来的问题就是要找到接收方,也就是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
。用图来表示就是下面这个样子:
那么问题来了,我们保活的目的是保证状态State
的生存,可为啥搞了半天却是给RenderObject
设了个标志位。它们之间又有什么关系呢?为理解这个问题,就不得不提到Sliver了。Flutter中涉及高效滚动的地方基本上都会用Sliver子框架。包括本文保活所需要的ListView
和TabBarView
。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.keepAlive
为true
的时候,该孩子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
的时候有一些细节就需要我们着重考虑了。
- 当需要保存状态的时候我是否只需要无脑设置
wantKeepAlive
为true
就完事了?
讨论这个问题的时候我们就要想到状态保活的代价是什么,而代价显然就是在显示范围之外的时候,保活的列表项对应的element子树和widget子树都是一直留存在内存中的,也就是占用更多的内存空间,如果我们的列表里有成千上万条数据,此时还是无脑设置wantKeepAlive
为true
的话那它们就都会赖在内存里不走了。如此一来,第一内存有可能爆掉,第二就是我还费事用sliver干啥,直接用SingleChildScrollView
不就结了吗,区别就是滑动过程中爆掉还是一开始就爆掉。
所以,正确的理念应该是只有真正需要保留状态的时候才将wantKeepAlive
置为true
,不需要的时候将其置为false
,以便回收。需要保留状态的列表项越少越好。
举个例子,比如我有个页面是城市选择列表,而这是个单选的列表,我选了北京就不能再选择上海,那么如果这个列表需要保留选择状态最好不要无脑设置wantKeepAlive
为true
,否则几百个市县有可能都会住在内存里不走了。此时更好的办法是只保留那一个选中城市的状态,其他的都默认wantKeepAlive
为false
。这样的话缓存起来的就只有一条了。所以Flutter也贴心的在AutomaticKeepAliveClientMixin
给我们提供了个函数updateKeepAlive
,以便在需要的时候更新保活状态:
@protected
void updateKeepAlive() {
if (wantKeepAlive) {
if (_keepAliveHandle == null)
_ensureKeepAlive();
} else {
if (_keepAliveHandle != null)
_releaseKeepAlive();
}
}
这个列表的例子我们只需要在选择状态发生变化的时候调用一下updateKeepAlive
更新保活状态。同时也不要再写死wantKeepAlive
为true
,需要改成类似下面的样子:
@override
// _selected表示选中状态,true为选中,false为未选中
bool get wantKeepAlive => _selected;
上面是个单选的例子,那如果我的列表是个多选的列表呢?比如邮件列表,我有可能要全选然后再做删除等等操作呢?
多选这种情况我们就要综合考虑了,如果是有限的多选,比如一次最多5条10条,那么可以参照上述单选的例子来处理,但是如果是不做限制的话,那可能这种动态设置的方式也没有减少内存占用,此时我们可以考虑将状态提升,即把选择状态提升到列表的父节点那里去,或者可以使用Provider
等状态管理工具来做提升。列表项自身不保存状态,这种情况下我们都不需要混入AutomaticKeepAliveClientMixin
,或者更进一步,就是完全使用StatelessWidget
而不用StatefulWidget
。
- 当你设置了
wantKeepAlive
为true/false
就一定会保留/不保留状态吗?
如果你的列表项里只有一个State
混入了AutomaticKeepAliveClientMixin
,那么这个State
就决定了是否保留状态,但是如果你的列表项是个复杂Widget
,里面有不止一个State
混入了AutomaticKeepAliveClientMixin
,那么每个State
调用_ensureKeepAlive
后会在_handles
里留下一个记录。而每次调用_releaseKeepAlive
则会从_handles
里去除对应的记录,但是这里要注意一点,就是只有在_handles
为空以后才会将_keepingAlive
置为false
。换句话说,在有多个State
的情况下,只有所有State
都声明不保留状态的情况下,才会将其回收,所以结论就是设置了wantKeepAlive
为true
就一定会保留状态,设置wantKeepAlive
为false
不一定就不保留状态,这得看看有没有其他State
仍然在要求保留。
另外还有一个要注意的点就是当存在sliver嵌套的情况下,设置保留状态只会对离自己最近的那个Sliver生效。比如你在TabBarView
里嵌套了个ListView
。然后在列表项里声明要保留状态,那么这个声明只会在ListView
里生效。如果切到别的tab
然后再切回来的话,状态也就丢失了。
总结
本文通过源码对AutomaticKeepAlive
做了详尽的介绍,相信看过以后大家对sliver中状态保留的原理有了一定的了解。然后又对于我们在使用AutomaticKeepAliveClientMixin
的时候可能会遇到的一些问题做了相应的分析,希望能对于使用状态保留的正确姿势有所帮助。
(全文完)
转载自:https://juejin.cn/post/6979972557575782407