前言
这篇文章呢,可以说是对最近flutter的listView研究做个小小的汇总;
其实呢,我一直对flutter的设计没太有什么好感,尤其是完成那个支持仿真翻页的小说阅读器的那段时期,不止一次的想:md这做的啥玩意,看看隔壁Android 的xxx,就你这还好意思对标Android原生?
PS:其实现在关于文字绘制这块我还是这么想的;
不过最近看了闲鱼的 Flutter 高性能、多功能的全场景滚动容器,一定要看!,有了些想法改进重写那个小说阅读器,研究了下,发现flutter的listView设计,好像还是蛮不错的,思路很清晰很轻量,相较之下,Android 就比较沉重了;(可能也是现在的listView太初期的缘故,不过设计思路倒是蛮好读的)
PPS:话说是不是闲鱼的自定义engine版本太低了,所以才没那些功能?像曝光、复用这块都已经有现成的部分了啊……为啥要费劲自定义呢?
演员就位,好戏开始
首先呢,默认大家都对flutter的一些基础知识、比如说三棵树及其作用啊什么的都已经了解;当然没了解的也没太大关系,百度谷歌下,这玩意的讲解已经烂大街了,看个5分钟了解下大概就够了;
PS:如果懒得看,直接翻后面总结部分,一步到胃;
因为widget树和element树并不参与绘制过程,所以相对轻量,所以在我看来,复用renderObject,即可提高很多性能,所以问题来了:
怎么去复用renderObject?
要解决这个问题,我们就要开始追踪下RenderObject跟element的爱恨情仇;
当然关于他俩的鸡毛蒜皮或陈谷子烂芝麻的事就不在这里提了,直接看相关的部分;
widget树?那玩意就是个舔狗,召之即来挥之即去的家伙,不用管;
众所周知,一个View要想展示,必须要走三步:measure、layout、draw;flutter中同样道理,只不过这事是renderObject来做的;在listView中找下相关方法就能找到相关部分:
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
/// Creates a sliver that places multiple box children in a linear array along
/// the main axis.
///
/// The [childManager] argument must not be null.
RenderSliverList({
required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
@override
void performLayout() {
…………一堆不相关的
/// 好哥哥看过来
bool advance() { // returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout)
inLayoutRange = false;
child = childAfter(child!);
if (child == null)
inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child!) != index) {
// We are missing a child. Insert it (and lay it out) if possible.
child = insertAndLayoutChild(childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child!.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child!.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
return true;
}
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// we want to make sure we keep the last child around so we know the end scroll offset
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent = childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
geometry = SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
);
return;
}
}
// Now find the first child that ends after our end.
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
……
collectGarbage(leadingGarbage, trailingGarbage);
……
}
}
注意其中的advance(),其中有个insertAndLayoutChild 方法,从字面上来看,就是它负责插入子RenderObject的
@protected
RenderBox? insertAndLayoutChild(
BoxConstraints childConstraints, {
required RenderBox? after,
bool parentUsesSize = false,
}) {
assert(_debugAssertChildListLocked());
assert(after != null);
final int index = indexOf(after!) + 1;
_createOrObtainChild(index, after: after);
final RenderBox? child = childAfter(after);
if (child != null && indexOf(child) == index) {
child.layout(childConstraints, parentUsesSize: parentUsesSize);
return child;
}
childManager.setDidUnderflow(true);
return null;
}
唉,其中有个_createOrObtainChild 方法,从字面上翻译,意思是,创建或获取child?获取child?
再点进去看看
void _createOrObtainChild(int index, { required RenderBox? after }) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
if (_keepAliveBucket.containsKey(index)) {
final RenderBox child = _keepAliveBucket.remove(index)!;
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
assert(childParentData._keptAlive);
dropChild(child);
child.parentData = childParentData;
insert(child, after: after);
childParentData._keptAlive = false;
} else {
_childManager.createChild(index, after: after);
}
});
}
下面那个createChild肯定不是缓存相关的了,所以这个_keepAliveBucket 嫌疑很大唉,看下他的方法内容,应该就是它做复用了;但是被复用的对象是谁加进来的呢?
点一下看下都是谁在用这个_keepAliveBucket
em,好像还不少人用,但是没有关西;反正我就想知道是谁往里面塞数据,这么再一看,只剩下两个方法:
一个是move方法,看名字就不像; 一个是_destroyOrCacheChild 方法,看上去就是它,点进去看看;
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
if (childParentData.keepAlive) {
assert(!childParentData._keptAlive);
remove(child);
_keepAliveBucket[childParentData.index!] = child;
child.parentData = childParentData;
super.adoptChild(child);
childParentData._keptAlive = true;
} else {
assert(child.parent == this);
_childManager.removeChild(child);
assert(child.parent == null);
}
}
所以又冒出一个新东西:SliverMultiBoxAdaptorParentData,那么是不是只要让这玩意的 keepAlive为true 就行了
先看下是谁在调用这个_destroyOrCacheChild;
唉,就一个方法 collectGarbage ,而这个方法只在performLayout 调用,跟那个_createOrObtainChild 方法一样,都是直接或间接在performLayout方法中调用,看来跟它一样,属于比较直接的生命周期类方法;
那么直接重写这个collectGarbage 方法,讲要回收的对象的keepAlive改为true,不就可以了么?
就这?????
最后试了下,好像还真是这样,创建过的RenderObject,不会再创建第二次,createChild 方法每个index只调用一次;应该是复用成功了;
拓展与猜想
复用问题就这么解决了?就这 两个字真的是我当时的想法,甚至感觉好像不会这么简单,是不是哪里有坑啊……
不过我因此产生了一些猜想:
如果这样的话,能否像RecyclerView那样彻底复用RenderObject?
现在是根据index来复用的,很简单;如果滑动到新的item,而且缓存中不存在,还是会走create;
但是如果像Android的RecyclerView那样,直接拿缓存区的RenderObject,然后重新塞回去替代创建,并通过更新机制更新展示数据,这样是否可行呢?记得看过某篇文章中说过,flutter内部更新使用一个diff算法来做的,挺高效的,这样用高效的diff算法应该比单纯创建高效吧,应该吧~~~
总结
复用这块直接用flutter官方提供的就完事了,当然纯属试验性质,有没有坑还真没试出来,也没做全面测试
这是我的flutter doctor -v 关于flutter部分的信息,如果你那代码有差别的话,可以对比参考下:
[√] Flutter (Channel stable, 1.22.5, on Microsoft Windows [Version 10.0.17763.1577], locale zh-CN)
• Flutter version 1.22.5 at D:\Program File\sdk\flutter\flutter_windows_v1.9.1+hotfix.2-stable
• Framework revision 7891006299 (9 weeks ago), 2020-12-10 11:54:40 -0800
• Engine revision ae90085a84
• Dart version 2.10.4
下面上关键部分代码,默认自定义的 RenderSliverList 已经通过继承和重写引入进去
class RecyclerRenderSliverList extends RenderSliverList {
RecyclerRenderSliverList({
@required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
@override
void collectGarbage(int leadingGarbage, int trailingGarbage) {
/// 如果从头开始要回收的垃圾数量+从尾开始要回收的垃圾数量 不等于 0(也就是大于0)
if (leadingGarbage + trailingGarbage != 0) {
print("collectGarbage : " +
" leadingGarbage : " +
leadingGarbage.toString() +
", trailingGarbage : " +
trailingGarbage.toString());
if (childCount >= leadingGarbage + trailingGarbage) {
int tempLeadingGarbage = leadingGarbage;
int tempTrailingGarbage = trailingGarbage;
RenderObject tempFirstChild = firstChild;
RenderObject tempLastChild = lastChild;
while (tempLeadingGarbage > 0) {
/// 标记keepAlive为true
(tempFirstChild.parentData as SliverMultiBoxAdaptorParentData)
.keepAlive = true;
tempFirstChild = childAfter(tempFirstChild);
tempLeadingGarbage -= 1;
}
while (tempTrailingGarbage > 0) {
/// 标记keepAlive为true
(tempLastChild.parentData as SliverMultiBoxAdaptorParentData)
.keepAlive = true;
tempLastChild = childBefore(tempLastChild);
tempTrailingGarbage -= 1;
}
}
}
/// 剩下的flutter都做好了
super.collectGarbage(leadingGarbage, trailingGarbage);
}
}
就这么简单…………
当然这块是完全复用,貌似没上限的那种,就是那种你要有一万个item,他就给你缓存一万个,所以理论上会非常吃内存,正确的做法应该要结合自己定义的缓存规则来做,不过那块还没搞~
所以还需要进一步测试研究~
另外补充一小点:
曝光这块其实也蛮简单的,这帮renderObject的parentData都是SliverMultiBoxAdaptorParentData ,里面都带上了index……
所以其实只要遍历下子child,看下保存的offset的数值,就可以得知当前第一个可见项什么的……然后直接从parentData中拿index就完事了
这个我是真的感觉没啥问题的
题外话
如果还有需要,可以拉下 flutter_novel 的dev分支,里面的reader2文件夹就是相关部分的,不过需要自己找,目前那块都是试验性质的,搞的其实有点乱;
在这里先给大家拜个年,如果不出意外的话,接下来的12天我就要蹲在提瓦特大陆了~所以嘛,失个联,很正常嘛,除非大家能在提瓦特大陆相遇,然后问候一句:
原来你也玩原神?
咳咳,真tmd尬