阅读 9305

ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案

ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案

学习最忌盲目,无计划,零碎的知识点无法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概化二十篇左右文章分析,欢迎关注,共同进步。![Flutter framework]

欢迎搜索公众号:进击的Flutter或者runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~

导语:

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

原理篇:

1、Widget、Element、Render树究竟是如何形成的?

2、ListView的构建过程与性能问题分析

实战篇:

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

2、ListView性能问题与优化方案

3、开源!!!!

ps: 组件目前已基本完成基本功能和性能上的优化,即将开源,关注点赞不要错过最新信息!!

上一期我们分析了ListView组件的功能设计,这一期我们着重从性能方向分析ListView卡顿的根本原因和优化方案。


一、ListView存在性能问题么?

日常业务开发中,我们会在多种场景下使用ListView组件。使用它可以快速完成一个列表页面,或者去适配一些小屏的设备。那么使用原生的ListView组件会存在性能上的问题么?答案是肯定的,在实际的业务场景中,我就遇到了这样一个页面(UI不长这样)

Screenrecording_20210315_110453.gif

这是一个竖向的列表,其中每一行的item是三个 TextField。如图中 Performance OverLay 显示一般,profile 模式下这个页面在我的 Vivo X23(骁龙 660)上出现了严重的卡顿。


二、为什么出现了卡顿?

基本原理

我们知道,对于滑动列表的这个过程,其实是由一个个的画面组成,术语称为。对于大部分人而言,当每秒的画面达到60,也就是俗称60FPS的时候,整个过程就是流畅的。而不及60FPS的时候,就会产生卡顿的感觉。

一秒 60 帧,也就意味着平均两帧之间的间隔为 16.7ms。如果超过 16.7ms,在观感上就会出现卡顿。通过系统提供的 DevToools 工具可以查看到,上面的例子中出现卡顿时一帧的耗时高达 130ms。

image.png

系统为了绘制一帧需要经历哪些阶段?

为什么一帧的耗时会超过16.7ms?为了搞清楚这个问题我们需要知道,Flutter为了绘制一帧会做些什么?

其实我们只需要在任意Flutter工程中,搜索drawFrame() 便可以得到答案。

image.png

这个方法上英文注释写得非常详细,推荐大家去看看。一共有10步骤,其中,与开发者关系比较密切的有下面几步。

image.png

动画->构建->布局->绘制->合成

发生卡顿的原因

结合DevTools的分析图,我们可以看出。在上面130ms的构建的主要耗时集中在Layout中调用的build方法

image.png

根据上面提到帧绘制的流程,我们可以看到,build了之后layout,这里为什么layout中又进行了build?

我在Flutter增强列表-ListView性能问题分析中提到过,Flutter中ListView采用懒加载机制。对于ListView里面的每一个item,并不会在build阶段全部进行构建。而是在layout阶段,根据屏幕当前的尺寸以及缓存区的范围,动态的构建每一个item,过程如图(图片来自upYang

所以上面的分析图中,layout主要是ListView进行布局,而其中的build是每一个child节点。其实仔细一点看,每一个build下面的结构基本都是KeyedSubTree、AutomaticKeepAlive、KeepAlive等,这是ListView在构建时,为每一个item包裹的,详细原理在Flutter增强列表-ListView性能问题分析有提到。

所以引起卡顿的原因非常明显主要由于,在某一帧内,ListView构建多个复杂的item。例如分析图中,在Layout阶段同时build了多个item,一个item的构建耗时已经接近10ms,同时构建自然超过了16ms。


三、哪些场景下容易出现卡顿?

既然我们知道了引起ListView的卡顿原因,那么卡顿主要会发生在什么时候?结合我自身的测试发现,主要在下面三个阶段。

  • 1、首次进入,列表构建时

当我们打开一个ListView构建的页面时,由于这时ListView中没有任何一个item,所以会进行多次的构建,上面例子的130ms就是如此。

  • 2、快速滑动,一帧内构建多个item

当我们在快速滑动的过程中,因为滑动范围比较大,同样可能引起多个item的构建。

image.png

  • 3、setState进行加载更多

第三个场景是在一些分页列表上,我们往往在数据请求完成后进行setState()更新列表,最终会调用到ListView对应Element的performRebuild()中

image.png

其中的_childElements是缓存的item节点(即当前屏幕上以及缓存区的所有item),这里会对每一个item进行update。同时,由于有了更多子节点(item数量增加),所以还会去构建新的item,同样容易引起卡顿。

image.png


三、如何优化ListView卡顿?

优化思路

1、分帧上屏

卡顿的本质原因是在一帧内,模块的运行时间过长,这不光是ListView的问题,所有有复杂元素的页面都一样。那么我们有没有一种通用的方案解决这个问题?其实答案很简单,我们可以从两条路去思考:第一种 优化模块时间(例如安卓上的布局优化等) 这个需要我们具体问题具体分析,因为导致模块卡顿的原因是多样的,有可能是Widget太复杂,没有合适的局部刷新,或者 UI isolate进行了大量计算等。第二条思路是在不优化模块的情况下,对时间进行分片,提升流畅度 也就是俗称的分帧运行,一张图了解原理:

image.png

假设,我们屏幕能显示4个item,每个item构建耗时是10ms。在现有的ListView布局过程中,会在第一帧的时候,同时构建这四个item,总共40ms。

采用分帧之后,在页面的第一帧我们先通过构建简单的占位item,占位的item可以是个简单的Container。由于其构建基本不耗时,在第一帧的时候构建四个Container不会导致卡顿。 之后将实际的四个item,分别延迟到后面四帧进行渲染。这样对于每个16.7ms而言,都没有发生超时渲染,整个流程不会发生卡顿

这样分帧上屏之后,会影响用户体验么?看看大厂怎么说:

在体验方面,前面讲列表控件结构时已知有一个不可见的 Cache 区域,所以分帧上屏大部分是在这个不可见区域完成的,为此在高端机或正常滑动情况下用户并无感知。而在低端机上快速滑动能明显看到卡片空白情况,但整体相比严重顿挫体感要好。

结合我这段时间的测试,这个方案确实对于高端机(测试机:一加7Pro)几乎没有影响(和实现方案有关),在中低端机优化明显,使用过程几乎不会出现卡顿。

2、LoadMore增量更新

上面我们提到了,item的构建是由ListView的layout驱动,所以如果是增量更新的情况,我们只要修改itemCount之后,标记ListView进行layout即可。闲鱼在文中提到了这个在layout之前需要做Widget缓存的更新,但是实际上在1.22之后,因为这个缓存几乎没有任何优化作用,官方已经去掉了这个Widget缓存,所以这个过程变得更加简单。

image.png

3、Element复用?

闲鱼在一文中还提到了一点:element的复用。这个优化点在和lwlizhe交流之后,我个人认为可能效果没那么明显。因为如果从Native的角度出发以ViewHolder为例,他的复用本质是对于同类型的item减少创建view和解析xml的时间,其中有个关键的方法:onBindViewHolder将数据绑定到View上。

但是对于Flutter而言,即使item的类型相同,对于不同数据的item而言,并没有一个数据绑定Widget的方法。所以仅仅只能做建立一个缓存池来保存element,创建的时候优先从缓存获取。但这样问题就来了,其实官方本来就有一个cacheExtent缓存区的设计,缓存在cacheExtent内的的Element。个人认为没多大必要额外在做一个缓存。最简单的,将cacheExtent设置大一点就行。

实现方案

有了基本思路如何来实现这个功能?主要和大家聊聊分帧上屏的实现。

分帧上屏简单来说就是占位和实际Widget的替换,但关键点在于如何分帧?

这里借助条件分帧任务队列实现,其原理如图所示。

image.png

首先,为了不影响系统本身的渲染过程,任务会被添加到当前isolate的队列中,这样在系统执行完渲染等事务之后开始调度任务。但是任务并非立刻执行,而是需要满足一定的条件,参考系统的做法,有一个权重值的枚举,我们为每个任务定义一个权重值,当满足对应的条件才可执行。

image.png

例如,如果我们的任务权重是Priority.idle时,这样的任务只会在完全空闲时刻执行(与定义的调度策略有关)。如果此时屏幕上有一个不间断的动画,那么整个task队列就会被阻塞。

其中的任务很简单,就是将占位Widget和实际的child进行替换,不过这里可玩性挺高。

最简单的方式就就是直接替换,为了突出加载过程,我将占位和实际的item改成了对比强烈的颜色,实际使用的时候,可以根据item的样式设置接近的占位Widget,效果更佳。

Screenrecording_20210315_133131.gif

这样看起来有点生硬,我们可以给他加个透明度的变化~

Screenrecording_20210315_133310.gif

或者来个从左向右的滑动入场~

Screenrecording_20210315_133848.gif

OHHHHHHHHHHH

其实可以看出整个过程,慢速滑动时,由于预加载,是看不到Widget的切换过程。而快速滑动过程中虽然偶尔出现一帧超时渲染,但是其峰值比以前低了接近一半,而且整体fps稳定了很多,滑动过程中并没有出现明显的顿感。

不过因为添加了很多动画,可能会导致部分UI卡顿~ HAHAHAHHA


四、非列表的卡顿解决

对于由构建类导致的卡顿,我们同样可以将复杂的一帧分解到多帧中优化卡顿,来个栗子康康:

image.png

复杂的页面肯定由复杂的元素组成组成,这里我们column下放了多个row,每个row中放入多个复杂的Widget。这样的例子中,我们可以对每一个row模块嵌套我们的分帧Widget,让每个row进行分帧渲染,实际优化效果如下图:

image.png

这是对于页面的横向优化,同样的可以通过这样的方式对复杂的Widget进行纵向优化,这里就不举例了~


五、目前profile模式下,性能优化数据:

首次进入 列表构建性能提升90%

image.png

快速滑动 几乎没有出现卡顿

image.png

加载更多性能提升80%

image.png

内存上的对比

ListView快速滑动 image.png BKListView快速滑动 image.png


六、最后 感谢各位吴彦祖和彭于晏的点赞

组件目前完成了功能和性能上的开发,为了确保组件的稳定性,还在进行方案review和边界测试,待验证无问题后再与大家见面,欢迎关注我获取最新动态。

当然就像前面提到,流畅度的优化我们可以走两条大方向:1、降低模块消耗的时间2、将时间做拆分。分帧是Flutter的一个优化思路,后面会进行更多方向的尝试。现在在规划一系列Flutter的优化思路,从监控到性能工具到优化,结合framework知识和大家分享实际能落地与项目的技术点。欢迎关注我的掘金和公众微信号,给你带来最干的知识点。

欢迎搜索公众号:进击的Flutter或者runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~

image.png

没想到吧凯多,我又回来啦!!!!!

文章分类
Android
文章标签