Flutter如何设计一个高性能,多功能的ListView组件

6,100 阅读10分钟

Flutter如何设计一个高性能,多功能的ListView组件

学习最忌盲目,无计划,零碎的知识点无法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概化二十篇左右文章分析,欢迎关注,共同进步。![Flutter framework] 欢迎搜索公众号:进击的Flutter或者runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~

导语:

最近因为在做Flutter中相关的性能优化,在收集很多性能数据之后发现ListView组件在一些场景下(例如加载更多)容易引起页面卡顿,看到了闲鱼的Flutter 高性能、多功能的全场景滚动容器。但奈何该组件没有开源,因此准备从文章给出的思路尝试研究和开发一个高性能的ListView。这个系列预计会分为4-5篇文章,前两篇主要对现有问题研究和分析,后三篇实际的进行开发。

实战篇:

1、Flutter如何设计一个高性能,多功能的ListView组件

2、如何解决特定场景下ListView中存在的性能问题

3、开源!!!!

PS:组件目前已经完成了功能上的开发,目前正在持续做性能上优化,即将开源,关注点赞不要错过最新信息!!

前两期的文章中,我们了解了Flutter中Widget、Element、Render是如何形成树结构?Flutter增强列表-ListView性能问题分析,熟悉了ListView控件的结构与构建过程。如果你是第一次阅读这个系列,也不要紧,这篇文章并不需要什么原理知识,我会偏向于从功能设计角度分享这个组件的实现思路,以及我对于该组件的设计架构,相信看完你也可以实现同样的功能!!


一、多功能的ListView组件需要提供哪些能力?

既然我们号称高性能,多功能的ListView,那这个组件该包含哪些能力?首先我会认为,无论做组件还是架构,我们的设计应该尽量保证每个模块的功能单一并且完善。虽然我们号称多功能,但是组件本质任然只是一个ListView,所以提供的能力应该是围绕可以滚动的列表出发。结合闲鱼的文章与个人的日常使用,我认为ListView还欠缺下面几种能力。

1、滚动到指定index

我们在Flutter中可以通过使用ScrollController控制ListView滚动到指定的位置,但这里的位置是基于offset(偏移像素)而非index,实际开发中我们常常会用到跳转指定index的能力。例如,我们想要实现tab与列表的联动,点击tab跳转到指定的列表位置。 这个时候,如果我们的跳转能基于index,那么这个功能就非常好实现了。

2、自动曝光能力

业务场景中,我们经常需要对列表中的item做曝光处理。当前,我们往往会在item的build函数或者initState中进行,但由于ListView的预加载和垃圾回收机制,一些未出现在屏幕上的会被提前曝光。对于曝光过的item可能因为被回收后进行二次构建,会再次走曝光逻辑。这就要求我们在业务代码中增加额外的逻辑,处理起来非常不合理。

曝光能力其实是获取屏幕上可见的item的衍生,所以同样的,组件也该包含这样的能力。

3、垃圾回收的回调通知

这点我们同事在实际的业务场景中遇到过,对于列表加载多图,即使划出屏幕的图片组件element被回收,但图片缓存任然累积在内存中,当时引起了大量的OOM,最后通过外界纹理的方案解决了这个问题。虽然我也认为,这样的问题应该在控件内部解决,但是如果有垃圾回收的回调通知,那么假如以后列表的item换成了视频,或者其他类型的控件,我们处理起来会更加灵活一点。

PS:(上面的功能都已经实现了~~)


二、高性能的ListView组件要解决哪些场景?

上面是对于功能的设计,那么从性能角度闲鱼在文章中也提到了我们遇到的一些问题:

1、LoadMore场景下的增量更新

我们在使用ListView的时候,往往会配合刷新组件做加载更多的功能。很多时候,我们都会在获取到更多数据,后调用setState更新列表UI,但调用setState之后,SliverMultiBoxAdaptorElement会对当前屏幕上以及缓存区中所有的element更新,在这个时间节点,非常容易引起列表的卡顿。

2、Element的缓存

因为列表在滚动过程中会不断的创建新的item和回收不可见的item(upYang:Flutter ListView 是如何管理 item 的? 对于被回收的item,下次再可见的时候又会被重新创建element。如果增加一个基于index的缓存,在上下滑动的场景下直接从缓存获取element应该能提升部分的流畅度。但要注意,这里的缓存,并非像原生那般能做到holder的复用,这个问题会在下一篇详细展开。

3、分帧上屏

对于复杂的item,使用一定的策略将他的构建分到几帧中,减缓列表构建时候的卡顿,在和lwlizhe的交流中有了思路。


三、每种功能的实现方案

在明确了功能需求之后,我并没有着急动手开发,而是先思考这些功能的在实现上的基本方案以及他们之间的联系(本期以功能分析为主,下期会进行性能上的分析)。

  • 滚动到指定的index 这个功能目前已经有很多的开源方案,我了解下来发现主要有两种思路:

1、重新构建视窗,指定我们需要跳转index的Widget到当前视窗的顶部。例如 indexed_list_view

这种方法思路比较简单,不过emmmm咋说呢,这效果也太粗暴了点吧。

2、缓存每个item的高度,指定滚动index的时候去计算需要滚动的offset 。例如list_view_item_builder

这个方案的核心思路是: 通过一个代理的Widget包裹我们实际的item,当item进行布局的时候将布局信息通知到Delegate进行缓存。需要滚动的时候通过delegate计算好offset后最后调用ScrollController进行滚动。不过由于Flutter的ListView使用懒加载机制(可以查看Flutter增强列表-ListView性能问题分析了解),可能指定的index不在当前屏幕范围,这样就不会存在尺寸信息。所以需要不停的滚动,直到找到我们需要的index为止。

这个思路挺不错,不过里面滚动逻辑写有些复杂,而且我在运行example的时候还出现了bug,对于超出屏幕的index,有时并不能直接跳转到我们需要item上。 (指定了section是6和5,多次跳转才成功)

但个人感觉这个方案稍微温柔一点,所以最终参考了这个思路,并且完全重新实现了这个能力。

  • 自动曝光能力(获取屏幕可见Widget)

自动曝光本质上是回调给使用者 我们当前屏幕上有哪些可见的Widget。基于我们获取到了每一个item的Size信息之后,这个问题就迎刃而解了。

我们把itme进行排列,将ListView想象成一个窗口。滑动的时候基于offset改变窗口的位置以显示不同的item。根据偏移量和窗口的高度我们可以得到 可视范围的起点和终点,再基于item的高度缓存信息,便可计算出当前屏幕上的item。为了减少这个方法频繁的计算,我们可以增加一个采样范围,当列表的滑动超过某个阈值的时候我们才会进行计算。再通过一个map记录已经被曝光过的item,确保每个item只会被曝光一次。

  • 垃圾回收的回调通知

    这点相对比较简单,因为虽然垃圾回收是从RenderSliverList中performLayout()调用的,但是最终任然会走到Element的void removeChild(RenderBox child)中。所以可以通过Element将每次被销毁调的child通知去释放资源。但是这个会和我们在性能优化中提到的Element复用有关,设计的时候也要考虑这个问题。


四、组件整体结构设计

首先我们看看当前ListView中主要的几个类之间关系

平时我们都是直接使用ListView,但要先实现我们上面提到的功能,我们需要对ListView进行深度的定制。例如,上面提到,要给每一个item嵌套一个代理Widget发送通知测量的尺寸信息,那么我们可以选择重写SliverChildBuilderDelegate的build方法,在其中对应插入我们需要嵌套的Widget。有了消息的发送者必然需要在这个结构中插入接受者,这里我参考了PageView的实现,选择嵌套到ListView中收集尺寸信息,将这个信息传递给自定义的ScrollController,由他实现指定index的滚动。

上面是最终的类关系图,为了区别系统的组件,我为所有涉及修改的类都加上了BK作为关键字(我对我司爱的深沉)。蓝色部分为主要修改的地方,在这个结构中最大的改变是,引入的一个新的成员BKNotifier,他的主要是为其他类提供itemCount的信息,以及用于我们对列表的item进行增,删操作的时候提高效率。

抛去他们的引用关系,从功能的角度上看,他们之间存在这样的关系(不同功能采用不同颜色的虚线)

如果你还想了解更多信息,欢迎评论区交流。


总结

最后放上一张目前已经实现的功能图~,所有功能正在验证中,性能还在开发~

增量更新下的性能数据,debug下时间从320ms->100ms,约60%+(时间不重要,release下不会这么耗时,要关注提升的效率)

目前组件开发已经已经进入尾声,争取两周之内和大家见面。


最后 感谢各位彭于晏 吴彦祖的点赞和评论!!!

本期主要从功能设计的角度分享我的思路。以前在做功能模块设计的时候,我往往会先陷入局部的细节,这样越做到后面会发现问题越多,大大的增加了整体上的实现难度。这次翻了翻大学的软件工程资料,尝试自顶向下的解决问题,遵循软件开发流程,考虑各个模块之间的联系,很多问题就暴露在了开始,整个开发过程流畅了许多。

下期将会介绍性能方面的优化,涉及一些原理上的内容,推荐阅读我之前对于原理部分的文章,希望能加深你对Flutter framework的理解。

PS:感谢各位彭于晏 吴彦祖的点赞和评论!!!

巨人最新话真的震撼我!!!!