HarmonyOS开发实战( Beta5版)滑动白块问题解决最佳实践

本文主要是介绍HarmonyOS开发实战( Beta5版)滑动白块问题解决最佳实践,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

当应用程序需要使用列表显示内容时,通常会使用List+LazyForEach组件来实现。但是列表中需要显示耗时加载的内容时,仅依靠List+LazyForEach不足以获得最优的用户体验。例如显示在线网络图片,在弱网以及快速滑动浏览的场景下,由于来不及完成图片加载、解码显示,列表中图片显示位置会出现白块占位符,影响用户浏览体验。

问题场景

假设开发者想要在应用中开发一个在线音乐显示列表,列表中每一个Item包含专辑封面、歌曲名称都要在线实时下载后再显示。专辑封面图片的下载和显示需要一些时间,具体取决于网络的通道质量、图像大小等因素。如果当前Item显示在屏幕上时,其对应的图像尚未加载完成,则将出现白块(图像的占位符)。为列表显示提供数据加载能力常用方法是使用LazyForEach。LazyForEach会在提供的数据源上进行迭代,并在每次迭代中创建相应的组件。当在列表组件中使用LazyForEach时,ArkUI框架会在列表的可见区域按需创建Item组件。当Item超出屏幕时,ArkUI框架会销毁并回收组件,以减少内存占用。目前仅List、Grid、Swpier和WaterFlow组件支持使用LazyForEach。

优化思路

动态预加载会根据历史任务加载耗时情况,动态调整屏幕可视区域外数据预取数量,配合懒加载设置,可在列表不断滑动时,屏幕可视区外实时更新列表数据,通过预取和预渲染数据提升列表滑动体验。

优化前代码示例

设置cachedCount=5

// ...
build() {Column() {List() {LazyForEach(this.dataSource, (item: SongInfoItem) => {ListItem() {ListItemComponent({ songInfo: item }).height(`${100 / this.ITEMS_ON_SCREEN}%`).margin({ left: 20, bottom: 20 })}})}.cachedCount(5).width("100%").height("100%").friction(0.4)}
}

处理白块问题的常用方案是使用LazyForEach的cachedCount属性来减少白块(设置cachedCount属性,可以支持列表预加载屏幕以外的Item项)。如上图所示,可以看到当用户在滑动列表时,依旧出现了很多白块。

如若使用更大的cachedCount值来解决,设置cachedCount=40

如上图所示,可以看到在滑动过程中白块确实变少了。但是新的问题出现了:与较小的cachedCount相比,首屏加载需要更长的时间,这同样影响用户使用体验。

过小的cachedCount值

过小的cachedCount值会导致列表预取的Item数量不足。当用户滑动列表时,后台可能来不及准备好足够多的预取项,特别是内容数据量大或网络条件特别差的时候,列表滑动过程就容易出现很多白块。

过大的cachedCount值

虽然较大的cachedCount值可以缓解缺少预取项的情况,但在列表没有滑动时,过大的cachedCount值可能导致可见区域的加载时间过长。这在首屏加载场景尤其明显:cachedCount越大,完成可见区域所需的加载耗时就越长,因为许多不可见Item需要被预取,占用资源。“首屏问题”在快速滑动后(使用ScrollBar快速滑动),屏幕加载Item也会出现耗时过长现象。cachedCount值越大,快速滚动后完成下载可见区域中项目所需的时间就越多。

在良好的网络中设置过大的cachedCount值可能会导致资源浪费:预取了过多的Item,导致额外的CPU、网络开销浪费。

结论:仅依靠cachedCount无法完美解决内容白块问题,需要引入一种能够动态适应外部条件变化(网络条件、内存变化等)的机制来解决这个问题。

优化指导

动态预加载根据历史任务加载耗时情况,动态调整屏幕可视区域外数据预取数量,配合懒加载设置,可在列表不断滑动时,屏幕可视区域外实时更新列表数据,通过预取和预渲染数据提升列表滑动体验。

Prefetcher支持应用动态自适应网络状态,通过提前下载一些图片或资源,确保相关资源在需要时能立即显示,以尽可能减少白块出现的概率。

LazyForEach懒加载可以通过使用Prefetcher来预取和预渲染数据,在使用Prefetcher后,除屏幕内显示的ListItem组件外,还会预先将屏幕可视区外的部分列表项数据进行预渲染和预取。这样当列表向下滑动时,会先显示预渲染组件,屏幕可视区外会动态调整预取范围。预取逻辑在Prefetcher的BasicPrefetcher类中实现,BasicPrefetcher支持预取和预渲染(图像解码、添加到组件树等)过程分离、自适应调整与获取范围、优先加载可视区域、以及取消不必要任务(快速滚动列表的场景下,智能取消不必要任务),其渲染过程如下:

1.首先请求n条数据,并在屏幕上显示m条数据。

2.当列表滑动,缓存列表项需要从屏幕可视区外进入可视区内时,此时显示预渲染组件,屏幕可视区外会动态调整预取范围,相比仅设置cachedCount提升了显示效率。

3.当列表不断滑动,屏幕可视区外实时更新列表项、更新预取数据和预渲染数据。

图1 动态预加载渲染过程示意图

优化后代码示例

实现DataSourcePrefetching类,继承IDataSourcePrefetching接口,并实现prefetch和cancel方法,如下代码所示(源码参考):

import { SongInfoItem } from '../model/LearningResource';
import { HashMap } from '@kit.ArkTS';
import fs from '@ohos.file.fs';
import { IDataSourcePrefetching } from '@kit.ArkUI';
import { rcp } from '@kit.RemoteCommunicationKit';let PREFETCH_ENABLED: boolean = false;
const CANCEL_CODE: number = 1007900992;
const IMADE_UNAVAILABLE = $r('app.media.startIcon')export default class DataSourcePrefetching implements IDataSourcePrefetching {private dataArray: Array<SongInfoItem>;private listeners: DataChangeListener[] = [];private readonly requestsInFlight: HashMap<number, rcp.Request> = new HashMap();private readonly session: rcp.Session = rcp.createSession();private readonly cachePath = getContext().getApplicationContext().cacheDir;constructor(dataArray: Array<SongInfoItem>) {this.dataArray = dataArray;}async prefetch(index: number): Promise<void> {PREFETCH_ENABLED = true;if (this.requestsInFlight.hasKey(index)) {throw new Error('Already being prefetched')}const item = this.dataArray[index];if (item.cachedImage) {return;}// 数据请求const request = new rcp.Request(item.albumUrl, 'GET');// 缓存网络请求对象,便于在需要取消请求的时候进行处理this.requestsInFlight.set(index, request);try {// 发送http请求获得响应const response = await this.session.fetch(request);if (response.statusCode !== 200 || !response.body) {throw new Error('Bad response');}// 将加载的数据信息存储到缓存文件中item.cachedImage = await this.cache(item.songId, response.body);// 删除指定元素this.requestsInFlight.remove(index);} catch (err) {if (err.code !== CANCEL_CODE) {item.cachedImage = IMADE_UNAVAILABLE;// 移除有异常的网络请求任务this.requestsInFlight.remove(index);}throw err as Error;}}cancel(index: number) {if (this.requestsInFlight.hasKey(index)) {// 返回MAP对象指定元素const request = this.requestsInFlight.get(index);// 取消数据请求this.session.cancel(request);// 移除被取消的网络请求对象this.requestsInFlight.remove(index);}}
}
// ...

在应用列表界面,首先创建DataSourcePrefetching、BasicPrefetcher对象,然后在List的onScrollIndex回调中调用BasicPrefetcher的visibleAreaChanged方法,传入List的可见区域起始坐标。至此完成代码的优化。源码参考

import { SongInfoItem } from '../model/LearningResource';
import DataSourcePrefetching from '../model/ArticleListData';
import { ObservedArray } from '../utils/ObservedArray';
import { ReusableArticleCardView } from '../components/ReusableArticleCardView';
import Constants from '../constants/Constants';
import { util } from '@kit.ArkTS';
import PageViewModel from '../components/PageViewModel';
import { BasicPrefetcher } from '@kit.ArkUI';@Entry
@Component
export struct LazyForEachListPage {@State collectedIds: ObservedArray<string> = ['1', '2', '3', '4', '5', '6'];@State likedIds: ObservedArray<string> = ['1', '2', '3', '4', '5', '6'];@State isListReachEnd: boolean = false;// 创建DataSourcePrefetching对象,具备任务预取、取消能力的数据源private readonly dataSource = new DataSourcePrefetching(PageViewModel.getItems());// 创建BasicPrefetcher对象,默认的动态预取算法实现private readonly prefetcher = new BasicPrefetcher(this.dataSource);build() {Column() {Header()List({ space: Constants.SPACE_16 }) {LazyForEach(this.dataSource, (item: SongInfoItem) => {ListItem() {Column({ space: Constants.SPACE_12 }) {ReusableArticleCardView({ articleItem: item })}}.reuseId('article')})}.cachedCount(5).onScrollIndex((start: number, end: number) => {// 列表滚动触发visibleAreaChanged,实时更新预取范围,触发调用prefetch、cancel接口this.prefetcher.visibleAreaChanged(start, end)}).width(Constants.FULL_SCREEN).height(Constants.FULL_SCREEN).margin({ left: 10, right: 10 }).layoutWeight(1)}.backgroundColor($r('app.color.text_background'))}
}
// ...

优化前后对比

本文案例中的长列表一屏可以加载6条数据,为了测试动态预加载方案与设置不同的cachedCount对应用性能的影响。来测试快速滑动场景下出现的白块数量、CPU开销占比以及首屏加载时长。如下对比场景设置数据cachedCount=5、cachedCount=40。最终,使用IDE的profiler工具检测下述指标,得到的数据如下所示:

滑动列表场景对比

cachedCount = 5cachedCount = 40动态预加载

数据设置首屏加载滑动过程白块数量
cachedCount=5首屏加载快滑动过程中白块很多
cachedCount=40首屏加载慢滑动过程中没有白块或很少
动态预加载首屏加载快滑动过程中没有白块或很少

CPU开销对比

利用Profiler工具分析得到相关trace图,追踪流程为应用侧的APP_LIST_FLING(列表从开始滚动到结束)的整个过程,从而观察应用的CPU占比。(注:不同设备特性和具体应用场景的多样性,所获得的性能数据存在差异,提供的数值仅供参考)

图2 cachedCount=5 CPU占比trace图

cachedCount=5的CPU占比为3.96%。

图3 cachedCount=40 CPU占比trace图

cachedCount=40的CPU占比为5.04%。

图4 动态预加载CPU占比trace图

动态预加载的CPU占比为4.12%。

数据设置CPU占比
cachedCount=53.96%
cachedCount=405.04%
动态预加载4.12%

首屏加载时长对比

利用Profiler工具分析得到相关trace图,追踪流程从Create Process(应用进程创建阶段)标签开始,到首屏全部图片加载完毕结束,从而观察应用的首屏加载时长。(注:不同设备特性和具体应用场景的多样性,所获得的性能数据存在差异,提供的数值仅供参考)

图5 cachedCount=5首屏加载时长trace图

cachedCount=5首屏加载时长为530.4ms

图6 cachedCount=40首屏加载时长trace图

cachedCount=40首屏加载时长为1.8s

图7 动态预加载首屏加载时长trace图

动态预加载首屏加载时长为545.5ms

数据设置首屏加载时长
cachedCount=5530.4ms
cachedCount=401.8s
动态预加载545.5ms

总结

从实验数据可以看出:

1)当cachedCount=5时,首屏加载时间短,滑动过程中出现大量白块,滑动时CPU占比较小。

2)当cachedCount=40时,首屏加载时间过长,滑动过程中并未出现白块,滑动时CPU占比较大。

3)当在cachedCount=5时的基础上设置动态预加载时,首屏加载时间短,滑动过程中并未出现白块,滑动时CPU占比较小。

因此当用户使用LazyForEach在线加载含有图片等数据量比较大的资源时,可以考虑使用动态预加载来预防弱网以及快速滑动场景中出现的白块问题。

动态预加载是在模拟弱网以及快速滑动的状态下加载数据测试而得出的数据结论。当利用网络数据来探讨LazyForEach代码如何进行网络数据的加载和优化时,可以使用动态预加载,使用动态预加载这项技术后,因将预取和预渲染分离且在滑动过程中实时更新列表项、预取数据和预渲染数据,故能在弱网和快速滑动场景中明显减少滑动过程中出现的白块现象。

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。 

为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:

希望这一份鸿蒙学习文档能够给大家带来帮助~


 鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频教程+学习PDF文档

(鸿蒙语法ArkTS、TypeScript、ArkUI教程……)

 纯血版鸿蒙全套学习文档(面试、文档、全套视频等)

                   

鸿蒙APP开发必备

​​

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线

这篇关于HarmonyOS开发实战( Beta5版)滑动白块问题解决最佳实践的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1122554

相关文章

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

mybatis和mybatis-plus设置值为null不起作用问题及解决

《mybatis和mybatis-plus设置值为null不起作用问题及解决》Mybatis-Plus的FieldStrategy主要用于控制新增、更新和查询时对空值的处理策略,通过配置不同的策略类型... 目录MyBATis-plusFieldStrategy作用FieldStrategy类型每种策略的作

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

linux下多个硬盘划分到同一挂载点问题

《linux下多个硬盘划分到同一挂载点问题》在Linux系统中,将多个硬盘划分到同一挂载点需要通过逻辑卷管理(LVM)来实现,首先,需要将物理存储设备(如硬盘分区)创建为物理卷,然后,将这些物理卷组成... 目录linux下多个硬盘划分到同一挂载点需要明确的几个概念硬盘插上默认的是非lvm总结Linux下多

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

Python Jupyter Notebook导包报错问题及解决

《PythonJupyterNotebook导包报错问题及解决》在conda环境中安装包后,JupyterNotebook导入时出现ImportError,可能是由于包版本不对应或版本太高,解决方... 目录问题解决方法重新安装Jupyter NoteBook 更改Kernel总结问题在conda上安装了

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

pip install jupyterlab失败的原因问题及探索

《pipinstalljupyterlab失败的原因问题及探索》在学习Yolo模型时,尝试安装JupyterLab但遇到错误,错误提示缺少Rust和Cargo编译环境,因为pywinpty包需要它... 目录背景问题解决方案总结背景最近在学习Yolo模型,然后其中要下载jupyter(有点LSVmu像一个

Goland debug失效详细解决步骤(合集)

《Golanddebug失效详细解决步骤(合集)》今天用Goland开发时,打断点,以debug方式运行,发现程序并没有断住,程序跳过了断点,直接运行结束,网上搜寻了大量文章,最后得以解决,特此在这... 目录Bug:Goland debug失效详细解决步骤【合集】情况一:Go或Goland架构不对情况二:

解决jupyterLab打开后出现Config option `template_path`not recognized by `ExporterCollapsibleHeadings`问题

《解决jupyterLab打开后出现Configoption`template_path`notrecognizedby`ExporterCollapsibleHeadings`问题》在Ju... 目录jupyterLab打开后出现“templandroidate_path”相关问题这是 tensorflo