为有牺牲多壮志,敢教日月换新天。

HarmonyOS:ArkUI性能优化实践(2)长列表加载性能优化

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤博客园地址:为敢技术(https://www.cnblogs.com/strengthen/ 
➤GitHub地址:https://github.com/strengthen
➤原文地址:https://www.cnblogs.com/strengthen/p/18493356
➤如果链接不是为敢技术的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ 

一、长列表优化概述
1、案例一:在一万条数据量下,分别使用ForEach和LazyForEach,在应用启动时候的性能对比,可以看到:使用ForEach的时候,在一万条数据量下,其明显会有一个白屏的出现。而使用LazyForEach,它整个加载和启动的速度会非常的快。

案例二:在一万条数据量下,使用For Each和LazyForEach进行滑动对比,可以明显看到,左边的手机在进行滑动的时候,会有一些明显的滑动白块,而右边这张图在滑动的时候,它滑动的相对来说比较丝滑一些。

2、针对长列表加载这一场景,对列表渲染时间、页面滑动帧率、应用内存占用等方面带来优化,提升性能和用户体验的手段有如下4种:

(1)、懒加载:提供列表数据按需加载能力,解决一次性加载长列表数据耗时长、占用过多资源的问题,可以提升页面响应速度。
(2)、缓存列表项:提供屏幕可视区域外列表项长度的自定义调节能力,配合懒加载设置可缓存列表项参数,通过预加载数据提升列表滑动体验。
(3)、组件复用:提供可复用组件对象的缓存资源池,通过重复使用已经创建过并缓存的组件对象,降低相同组件短时间内频繁创建和销毁的开销,提升组件渲染效率。
(4)、布局优化:使用扁平化布局方案,减少视图嵌套层级和组件数,避免过度绘制,提升页面渲染效率。

3、以“HarmonyOS世界”中首屏的长列表加载为例,对比分析如下三个关键指标:

(1)、完全显示所用时间(Time To Full Display,TTFD):表示应用生成具有完整内容的第一帧所用的时间,包括在第一帧之后异步加载的内容。

(2)、丟帧率(Janky Frames):表示一个时间周期内的丢帧比率,是指一个时间周期内有问题的帧比例。HarmonyOS系统要求每一帧都要在11.1ms(90Hz刷新率)内绘制完成,如果页面没有在11.1ms内完成这一帧的绘制,就会出现丢帧。部分丢帧一般用户肉眼是感知不到的,只有出现连续丢帧用户才有明显感知。

(3)、独占内存(Unique Set Size,USS):一个进程所占用的私有内存,即该进程独占的内存。它反映了运行一个特定进程真实的边际成本(增量成本)。

 4、了解完四个优化措施,和三个关键衡量指标后,现在就可以进行长列表的渐进式的性能优化。

二、优化-1:懒加载,提供列表数据按需加载能力,解决一次性加载长列表数据耗时长、占用过多资源的问题。
1、在HarmonyOS应用框架中,实际上为数据加载,提供了两种渲染方式。
(1)、使用ForEach循环渲染:适合于对小数据量的渲染。
(2)、使用LazyForEach数据懒加载:适合于大数据量大渲染。

 2、ForEach和LazyForEach具体逻辑和区别:

(1)、ForEach的渲染逻辑:在HarmonyOS系统中,有三棵树的概念,

第一棵树:代表数据源。第二棵树:代表组件树。第三棵树:代表渲染页面。
ForEach渲染流程表示:它会从列表的数据源中一次性的取全量的数据,加载到这样的一棵组件树上,所以第一棵树,它是全量的进行挂载到第二棵组件树上,当数据量比较少的时候,一次数据加载它的性能并不会成为瓶颈,但是,当有上万条数据的时候,如果一次性要把这上万条数据挂载到组件树上,这必然会带来一些性能的问题,实际上再来看一下第三棵树,是否有必要一次性的去挂载所有的树呢?答案是否定的。实际上在这个渲染页面,可视区域只会展示这三个数据,所以(n - 3)个数据的渲染实际上是多余的,并不需要一次性全量加载这样一些数据。

(2)、LazyForEach实现了按需加载。可视区域只有三条数据,就可以按需只加载可视区域的三条数据,其他的(n - 3)条数据就不进行加载,这样数据源到组件树的挂载到时间可以进行大大的节省,这样就可以大幅的提高应用的性能。特别是当数据量比较大的时候,这个提升效果是非常明显的。

 3、对比案例:不同数据量下ForEach和LazyForEach性能对比。

把上面的表数据绘制成一张图。可以看到,在百余条数据的时候,ForEach和LazyForEach,它的性能对比差异 并不是特别的明显,当然LazyForEach此时还是比ForEach稍微好一点。但是,当数据量特别大,特别是达到上万条数据的时候,可以看到,ForEach有一个明显的变化,而LazyForEach还是保持着较好的性能。这个案例也提醒开发者,当数据量特别大的时候,需要正确的选择使用LazyForEach进行数据的加载,当数据量比较小的时候,实际上选择ForEach去进行列表的加载就足够了,因为ForEach相比LazyForEach,它在代码实现和代码书写的代码行数数量上来看,还是会简洁一点。开发者可以根据数据量大小,正确地去使用ForEach和LazyForEach。

遗留问题:滑动白块现象。下图中,页面一次可以显示3条数据,如果不提前缓存部分数据,当下滑到列表最底端时,再快速下滑,可能会引起“滑动白块”的现象。这是因为上一次只请求了屏幕上的3条数据,如果滑动速度过快,则会导致数据来不及加载而出现白块。在追求极致性能的同时,应该避免这样糟糕的用户体验。

三、优化-2:缓存列表项,通过预加载数据提升列表滑动体验。
为了解决滑动白块现象,可以进行第2项优化:添加缓存列表项。如何进行缓存列表项的添加:可以在可视区域上下,添加一个缓存区,实际上LazyForEach懒加载,可以通过设置cachedCount来指定缓存的数量。在设置了cachedCount的情况下,这样一个列表上就会添加一个缓冲区,那这样一些数据就会被提前的预加载。LazyForEach懒加载可以通过设置cachedCount来指定缓存数量,在设置cachedCount后,除屏幕内显示的Listltem组件外,还会预先将屏幕可视区外指定数量的列表项数据缓存。这样当一个屏幕数据加载完成后,再次向下滑动时,会先加载上一次请求的数据,加载完成后,再加载本次请求的数。如下图:这个案例中添加了缓存列表项以后,就会预先提前加载(n - 1)和(n + 3)两条数据,当这个列表快速滑动的时候,不管是向上滑动,还是向下滑动,显示的数据都是之前挂载到组件树上的数据。这样就会减少这样的一个滑动白块现象的产生。

LazyForEach懒加载,可以通过设置cachedCount来指定缓存的数量。如果屏幕显示n条数据,一般将 cachedCount的值设置为n/2。

这里做了一个实验对比,一个页面上展示的是6条数据,当cachedCount的值设置为3的时候,可以看到它的丢帧率是最小的。当cachedCount的值设置的更大,特别是特别大的时候,并没有带来更好的性能提升,反而会出现一定的帧率下降。开发者需要在实际开发中,合理的去设置这样的一个cachedCount值,可以根据实际场景进行合理的设置。比如:

(1)、当网络比较慢的时候,为了提升列表的浏览和用户体验,可以适当的多缓存一些数据,推荐把cachedCount的值设置为大于n/2。

(2)、当网络比较好的时候,列表又需要去加载一些大图或者视频的时候,大图和视频这样的一些数据,它会占用很多的内存,出于减少内存的考虑 ,推荐把cachedCount的值设置为小于n/2。

在实际场景中,需要去不断地调试,设置合适的这样的一个缓存列表的数据项,来达到性能和体验的最佳平衡。

四、优化-3:组件复用,提供可复用组件对象的缓存资源池,降低相同组件短时间内频繁创建和销毁的开销。

1、HarmonyOS应用框架提供了组件复用能力,可复用组件从组件树上移除时,会进入到一个回收缓存区。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。尤其在列表等场景下,其自定义子组件具有相同的组件布局结构,列表更新时仅有状态变量等数据差异。通过组件复用可以提高列表页面的加载速度和响应速度。

2、组件复用逻辑:当(m-1)划出这个缓冲区域后,会变成(m-2),当(m-2)即将销毁的时候,只销毁这个列表的数据,而保留这样一个列表的结构。比如:在这样一个列表结构,是两个文本和一张图片,可以把这样一种结构存储到回收缓存区中,如果(m+4)这样一个样式结构,正好和(m-2)的样式结构是一致的,就可以把(m-2)的样式进行复用。需要说明的是,不仅仅只是可以复用一种样式,样式是多种也可以复用。如下图,在绿色区域,画了一个堆叠的效果。即如果(m+4)样式和(m-2)的样式不一致,也可以复用缓存区的其他的一些跟(m+4)一样的样式。这个回收区,实际上是一个key-value的集合。如果把一个列表项进行复用了以后,就不需要重新的创建,从而节省了列表创建的时间。复用的是样式,填充的是数据。

 3、组件复用关键代码,比如:有这样一种组件列表样式,叫做ArticleCardView,如果要复用这样一个ArticleCardView,实际上需要完成以下组件复用三个步骤:

(1)、把ArticleCardView,设置装饰器@Reusable标识,表示其具备组件复用的能力。

(2)、实现自定义组件的生命周期回调函数aboutToReuse,将这个ArticleCardView里面,需要复用的一些数据如何进行填充。

(3)、设置一个可复用组件的reuseId,如果有多个可复用的组件,需要设置多个reuseId。

因为每一种样式都是key-value的形式,其中key值就是ArticleCardView的reuseId,推荐以组件名字设置为key值,即设置为reuseId。

4、组件复用前长列表性能分析。

一万条数据,均匀匀速的滑动列表,在滑动这个列表的过程中,可以发现,存在一个均匀的丢帧的一个现象。下图中的这些绿色区域, 实际上是一些不丢帧。红色区域,就是一些丢帧,红色均匀有规律的重复的出现。

这时因为列表在滑动的时候,会均匀匀速的去进行列表的创建,每一个创建,都是一个耗时的操作,都可能会出现一个丢帧的现象。拉大红色区域,见下图,最关键的耗时,是在BuildLazyItem操作,其耗时是10.277ms。上文中已经提到:HarmonyOS系统要求每一帧都要在11.1ms(90Hz刷新率)内绘制完成,如果页面没有在11.1ms内完成这一帧的绘制,就会出现丢帧。在本案例中,仅BuildLazyItem操作其耗时就达到了10.277ms,非常接近11.1ms。

下图中,再看一下这一帧的总耗时,达到了13.43ms,确实超过了11.1ms。其中消耗最大的部分就是BuildLazyItem操作,

5、组件复用后长列表性能分析。

按照上文提供的组件复用三个步骤,给ArticleCardView添加@Reusable注解,包括启用相关的复用代码后。在用同样匀速去滑动这个列表,在整个测量时间15.8s内,未出现丢帧的红色区域。

如下图,继续看一下某一帧的时间,BuildLazyItem耗时是0.749ms,远小于组件未复用的时候的10.277ms。BuildRecycle耗时0.222ms,两者相加小于11.1ms。

组件复用前、后长列表性能对比:

五、优化-4:布局优化,使用扁平化布局方案,减少视图嵌套层级和组件数,避免过度绘制,提升页面渲染效率。

列表不同于其他布局,包含了大量重复循环的Listltem,所以对每一个Listltem的布局优化格外重要。错误的布局方式可能会导致组件树和嵌套层数过多,在创建和布局绘制阶段产生较大的性能开销,导致界面卡顿。合理使用布局,减少嵌套层数,能提高布局效率。

如下图案例,进行一个正常的书写,它可能是一个5层的数据结构,可以通过相对布局,进行布局实现。

布局刻意嵌套。将布局进行嵌套20层,即在它的item的外层嵌套30个空白的嵌套,分析正常情况和过度嵌套情况下应用独占内存、页面滑动帧率、丢帧率的情况对比:

正常的书写,将这样一个最大的布局的嵌套层级维持在5~8层即可,也没有必要刻意的去优化。当然也不应该写出一些不好的代码。

 

posted @ 2024-10-22 17:27  为敢技术  阅读(6)  评论(0编辑  收藏  举报