学习最忌盲目,无计划,零碎的知识点无法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概化二十篇左右文章分析,欢迎关注,共同进步。![Flutter framework]
欢迎搜索公众号:进击的Flutter或者runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~
实战篇:
1、Flutter如何设计一个高性能,多功能的ListView组件
2、如何解决特定场景下ListView中存在的性能问题
3、开源!!!!
PS:组件目前已经完成了功能上的开发,目前正在持续做性能上优化,即将开源,关注点赞不要错过最新信息!!
既然我们号称高性能,多功能的ListView,那这个组件该包含哪些能力?首先我会认为,无论做组件还是架构,我们的设计应该尽量保证每个模块的功能单一并且完善。虽然我们号称多功能,但是组件本质任然只是一个ListView,所以提供的能力应该是围绕可以滚动的列表出发。结合闲鱼的文章与个人的日常使用,我认为ListView还欠缺下面几种能力。
我们在Flutter中可以通过使用ScrollController控制ListView滚动到指定的位置,但这里的位置是基于offset(偏移像素)而非index,实际开发中我们常常会用到跳转指定index的能力。例如,我们想要实现tab与列表的联动,点击tab跳转到指定的列表位置。
这个时候,如果我们的跳转能基于index,那么这个功能就非常好实现了。
业务场景中,我们经常需要对列表中的item做曝光处理。当前,我们往往会在item的build函数或者initState中进行,但由于ListView的预加载和垃圾回收机制,一些未出现在屏幕上的会被提前曝光。对于曝光过的item可能因为被回收后进行二次构建,会再次走曝光逻辑。这就要求我们在业务代码中增加额外的逻辑,处理起来非常不合理。
曝光能力其实是获取屏幕上可见的item的衍生,所以同样的,组件也该包含这样的能力。
这点我们同事在实际的业务场景中遇到过,对于列表加载多图,即使划出屏幕的图片组件element被回收,但图片缓存任然累积在内存中,当时引起了大量的OOM,最后通过外界纹理的方案解决了这个问题。虽然我也认为,这样的问题应该在控件内部解决,但是如果有垃圾回收的回调通知,那么假如以后列表的item换成了视频,或者其他类型的控件,我们处理起来会更加灵活一点。
PS:(上面的功能都已经实现了~~)
上面是对于功能的设计,那么从性能角度闲鱼在文章中也提到了我们遇到的一些问题:
我们在使用ListView的时候,往往会配合刷新组件做加载更多的功能。很多时候,我们都会在获取到更多数据,后调用setState更新列表UI,但调用setState之后,SliverMultiBoxAdaptorElement会对当前屏幕上以及缓存区中所有的element更新,在这个时间节点,非常容易引起列表的卡顿。
在明确了功能需求之后,我并没有着急动手开发,而是先思考这些功能的在实现上的基本方案以及他们之间的联系(本期以功能分析为主,下期会进行性能上的分析)。
1、重新构建视窗,指定我们需要跳转index的Widget到当前视窗的顶部
。例如 indexed_list_view。
这种方法思路比较简单,不过emmmm咋说呢,这效果也太粗暴了点吧。
2、缓存每个item的高度,指定滚动index的时候去计算需要滚动的offset
。例如list_view_item_builder
这个思路挺不错,不过里面滚动逻辑写有些复杂,而且我在运行example的时候还出现了bug,对于超出屏幕的index,有时并不能直接跳转到我们需要item上。
(指定了section是6和5,多次跳转才成功)
但个人感觉这个方案稍微温柔一点,所以最终参考了这个思路,并且完全重新实现了这个能力。
自动曝光本质上是回调给使用者 我们当前屏幕上有哪些可见的Widget。基于我们获取到了每一个item的Size信息之后,这个问题就迎刃而解了。
我们把itme进行排列,将ListView想象成一个窗口。滑动的时候基于offset改变窗口的位置以显示不同的item。根据偏移量和窗口的高度我们可以得到 可视范围的起点和终点,再基于item的高度缓存信息,便可计算出当前屏幕上的item。为了减少这个方法频繁的计算,我们可以增加一个采样范围,当列表的滑动超过某个阈值的时候我们才会进行计算。再通过一个map记录已经被曝光过的item,确保每个item只会被曝光一次。
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:感谢各位彭于晏 吴彦祖的点赞和评论!!