【入门到精通】鸿蒙next开发:基于ArkUI页面抛滑白块优化解决方案_imageknife性能优化
往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)
-
鸿蒙开发核心知识点,看这篇文章就够了
-
最新版!鸿蒙HarmonyOS Next应用开发实战学习路线
-
鸿蒙HarmonyOS NEXT开发技术最全学习路线指南
-
鸿蒙应用开发实战项目,看这一篇文章就够了(部分项目附源码)
基于ArkUI页面抛滑白块优化解决方案
简介
使用imageKnife后仍存在滑动白块问题的场景,常规的解决方案是设置更大的cachedCount缓存数量,但这种方案可能会导致首屏白屏和内场占用增多,针对这个问题,本文将主要提供一种动态预加载的方案,首先介绍相关原理,针对两种技术组合即LazyForeEach+ImageKnife、Repeat+ImageKnife,再分别结合prefetch提供对应场景的开发案例,最终对比不同方案的测试数据。
原理介绍
Imageknife原理介绍
ImageKnife是专门为OpenHarmony打造的一款图像加载缓存库,它封装了一套完整的图片加载流程,开发者只需根据ImageKnifeOption配置相关信息即可完成图片的开发,降低了开发难度,提升了开发效率。
详细介绍可参考:https://gitee.com/openharmony-tpc/ImageKnife
LazyForeEach原理介绍
LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
详细介绍可参考:文档中心
Repeat原理介绍
Repeat组件不开启virtualScroll开关时,Repeat基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在Repeat父容器组件中的子组件。Repeat循环渲染和ForEach相比有两个区别,一是优化了部分更新场景下的渲染性能,二是组件生成函数的索引index由框架侧来维护。
Repeat组件开启virtualScroll开关时,Repeat将从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了Repeat,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会缓存组件,并在下一次迭代中使用。
详细介绍可参考:文档中心
prefetcher原理介绍
prefetch是一种动态预加载技术,通过考虑滚动速度、屏幕上的项目数量等因素,动态的下载或取消下载资源,确保相关资源在需要时能立即显示。
详细介绍可参考:OpenHarmony三方库中心仓
页面抛滑白块优化解决方案原理介绍
使用LazyForEach/Repeat遍历数据项,通过实现Prefetcher接口监听数据项,选择合适的时机预取数据,使用ImageKnife三方库实现具体的预取功能,并管理缓存。
场景案例
本解决方案针对两种技术组合即LazyForeEach+ImageKnife+prefetch(首页)、Repeat+ImageKnife+prefetch(分类),其中提供对应场景的开发案例,界面效果。
关键代码如下:
1、Prefetcher结合LazyForeEach实现瀑布流页面关键代码
private readonly dataSource = new DataSource(); // 创建数据源 private readonly prefetcher = createPrefetcher() // 创建prefetcher .withDataSource(this.dataSource) // 绑定数据源 .withAddItemsCallback(() => { // 增加数据源的回调函数 this.dataCount = this.dataSource.totalCount(); if (this.addItemsCount { this.dataSource.batchAdd(this.dataSource.totalCount()); }, 1000); } }) .withAgent(new ImageKnifeWaterFlowInfoFetchingAgent()); // 绑定获取数据源项引用的数据的代理 build() { Column({ space: CommonConstants.SPACE_EIGHT }) { Column() { WaterFlow({ footer: this.footStyle, scroller: this.waterFlowScroller }) { LazyForEach(this.dataSource, (item: WaterFlowInfoItem) => { // 瀑布流中使用LazyForEach遍历数据源 FlowItem() { WaterFlowImageView({ waterFlowInfoItem: item, waterFlowItemWidth: this.waterFlowItemWidth }) } .height(item.waterFlowHeadInfo.height / item.waterFlowHeadInfo.width * this.waterFlowItemWidth + this.getTitleHeight(item.waterFlowDescriptionInfo.title)) // 通过固定宽高比计算卡片的高度 .backgroundColor(Color.White) .width($r(\'app.string.full_screen\')) .clip(true) .borderRadius($r(\'app.float.rounded_size_16\')) }); } .cachedCount(3) .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean) => { // 根据瀑布流卡片可见区域变化,调用prefetch的start()和stop()接口 if (isVisible) { this.prefetcher.start(); } else { this.prefetcher.stop(); } }) .onScrollIndex((start: number, end: number) => { // 列表滚动触发visibleAreaChanged,实时更新预取范围,触发调用prefetch、cancel接口 this.prefetcher.visibleAreaChanged(start, end); }) .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST }) .onReachEnd(() => { this.listenNetworkEvent(); }) .columnsTemplate(new BreakpointType({ sm: BreakpointConstants.GRID_NUM_TWO, md: BreakpointConstants.GRID_NUM_THREE, lg: BreakpointConstants.GRID_NUM_FOUR }).getValue(this.currentBreakpoint)) .columnsGap($r(\'app.float.water_flow_column_gap\')) .rowsGap($r(\'app.float.water_flow_row_gap\')) .layoutDirection(FlexDirection.Column) .itemConstraintSize({ minWidth: $r(\'app.string.zero_screen\'), maxWidth: $r(\'app.string.full_screen\'), minHeight: $r(\'app.string.zero_screen\'), }); } .width($r(\'app.string.full_screen\')) .height($r(\'app.string.full_screen\')); } .height($r(\'app.string.full_screen\')) .margin({ top: $r(\'app.float.margin_8\'), bottom: $r(\'app.float.navigation_height\'), left: new BreakpointType({ sm: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_LEFT_SM, md: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_LEFT_MD, lg: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_LEFT_LG }).getValue(this.currentBreakpoint), right: new BreakpointType({ sm: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_RIGHT_SM, md: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_RIGHT_MD, lg: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_RIGHT_LG }).getValue(this.currentBreakpoint) }) .animation({ duration: CommonConstants.ANIMATION_DURATION_TIME, curve: Curve.EaseOut, playMode: PlayMode.Normal }); }
2、Prefetcher结合Repeat实现瀑布流页面关键代码
@Local private readonly items: WaterFlowInfoItem[] = wrapArray([]); private prefetcher = createPrefetcher() .withDataSource(this.items) // 绑定数据源 .withAddItemsCallback(async () => { // 新增数据回调 this.pageIndex = (this.pageIndex++) % CommonConstants.REPEAT_WATER_FLOW_PAGES; const waterFlowInfoItemArray = await this.addData(CommonConstants.MOCK_INTERFACE_WATER_FLOW_FILE_NAME, this.pageIndex, CommonConstants.WATER_FLOW_PAGE_SIZE); this.items.push(...waterFlowInfoItemArray); }) .withAgent(new ImageKnifeWaterFlowInfoFetchingAgent()); // 预加载资源代理 build() { Column() { WaterFlow({ footer: this.footStyle, scroller: this.waterFlowScroller }) { Repeat(this.items) .each((obj: RepeatItem) => { FlowItem() { WaterFlowItemComponent({ waterFlowInfoItem: obj.item, waterFlowItemWidth: this.waterFlowItemWidth }) } .height(obj.item.waterFlowHeadInfo.height / obj.item.waterFlowHeadInfo.width * this.waterFlowItemWidth + this.getTitleHeight(obj.item.waterFlowDescriptionInfo.title)) .backgroundColor(Color.White) .width($r(\'app.string.full_screen\')) .clip(true) .borderRadius($r(\'app.float.rounded_size_16\')) }) .key((item: WaterFlowInfoItem) => { return item.key; }) } .cachedCount(3) .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean) => { // 根据瀑布流卡片可见区域变化,调用prefetch的start()和stop()接口 if (isVisible) { this.prefetcher.start(); } else { this.prefetcher.stop(); } }) .onScrollIndex((start: number, end: number) => { // 列表滚动触发visibleAreaChanged,实时更新预取范围,触发调用prefetch、cancel接口 this.prefetcher.visibleAreaChanged(start, end); }) .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST }) .onReachEnd(() => { this.listenNetworkEvent(); }) .columnsTemplate(new BreakpointType({ sm: BreakpointConstants.GRID_NUM_TWO, md: BreakpointConstants.GRID_NUM_THREE, lg: BreakpointConstants.GRID_NUM_FOUR }).getValue(this.currentBreakpoint)) .columnsGap($r(\'app.float.water_flow_column_gap\')) .rowsGap($r(\'app.float.water_flow_row_gap\')) .layoutDirection(FlexDirection.Column) .itemConstraintSize({ minWidth: $r(\'app.string.zero_screen\'), maxWidth: $r(\'app.string.full_screen\'), minHeight: $r(\'app.string.zero_screen\'), }); } .height($r(\'app.string.full_screen\')) .margin({ top: $r(\'app.float.margin_8\'), bottom: $r(\'app.float.navigation_height\'), left: new BreakpointType({ sm: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_LEFT_SM, md: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_LEFT_MD, lg: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_LEFT_LG }).getValue(this.currentBreakpoint), right: new BreakpointType({ sm: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_RIGHT_SM, md: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_RIGHT_MD, lg: BreakpointConstants.SEARCHBAR_AND_WATER_FLOW_MARGIN_RIGHT_LG }).getValue(this.currentBreakpoint) }) .animation({ duration: CommonConstants.ANIMATION_DURATION_TIME, curve: Curve.EaseOut, playMode: PlayMode.Normal }); }
3、Prefetcher必须要实现的接口
-
IDataReferenceItem接口:要与Prefetcher一起使用的数据源项的接口。用作预取器数据源的数据源项或数组元素实现此接口。
核心代码:
export type FetchParameters = string; export type PathToResultFile = string; const IMAGE_UNAVAILABLE = $r(\'app.media.default_image\'); @Observed export class WaterFlowInfoItem implements IDataReferenceItem { private static nextKey = -1; private _key: number = WaterFlowInfoItem.getKey(); private static getKey() { return ++WaterFlowInfoItem.nextKey; } public waterFlowHeadInfo: WaterFlowHeadInfo; public waterFlowDescriptionInfo: WaterFlowDescriptionInfo; cachedImage: ResourceStr = \'\'; get key(): string { return this._key.toString(); } regenerateKey() { this._key = WaterFlowInfoItem.getKey(); } constructor(info: WaterFlowInfo) { this.waterFlowHeadInfo = info.waterFlowHead; this.waterFlowDescriptionInfo = info.waterFlowDescription; } // 预取完成时的回调函数 onFetchDone(result: PathToResultFile): void { this.cachedImage = result; } // 预取失败时的回调函数 onFetchFail(_details: Error): void { this.cachedImage = IMAGE_UNAVAILABLE; } // 获取需要预取的资源链接 getFetchParameters(): FetchParameters { return this.waterFlowHeadInfo.source; } // 判断是否需要预取 hasToFetch(): boolean { return !this.cachedImage; } }
-
ITypedDataSource接口:可以链接到Prefetcher的数据源的接口。不需要修改数据源的实际实现。该接口确保数据源项类型与获取代理类型 IFetchAgent 匹配。
核心代码:
export class DataSource implements ITypedDataSource { private data: WaterFlowInfoItem[] = []; private readonly notifier: Notifier; private netWorkUtil: NetworkUtil = new NetworkUtil(); private pageIndex: number = CommonConstants.NUMBER_DEFAULT_VALUE constructor(notificationMode: NotificationMode = \'data-set-changed-method\') { this.notifier = new Notifier(notificationMode); } // 具体的添加数据方法 async addData(fileName: string, pageNo: number, pageSize: number): Promise { let waterFlowInfoArray: WaterFlowInfo[] = await this.netWorkUtil.getWaterFlowData(CommonConstants.MOCK_INTERFACE_PATH_NAME, fileName, pageNo, pageSize); for (let index = 0; index CommonConstants.MAX_NAME_LENGTH) { waterFlowInfoArray[index].waterFlowDescription.userName = waterFlowInfoArray[index].waterFlowDescription.userName.substring(0, CommonConstants.MAX_NAME_LENGTH); } this.data.push(new WaterFlowInfoItem(waterFlowInfoArray[index])); } return this.data; } // 外部调用的添加数据的方法 async batchAdd(startIndex: number) { this.pageIndex = (this.pageIndex++) % CommonConstants.LAZY_FOREACH_WATER_FLOW_PAGES; const items = await this.addData(CommonConstants.MOCK_INTERFACE_WATER_FLOW_FILE_NAME, this.pageIndex, CommonConstants.WATER_FLOW_PAGE_SIZE); this.data.splice(startIndex, 0, ...items); this.notifier.notifyBatchUpdate([ { type: DataOperationType.ADD, index: startIndex, count: items.length, key: items.map((item) => item.key) } ]); } getData(index: number): WaterFlowInfoItem { return this.data[index]; } totalCount(): number { return this.data.length; } registerDataChangeListener(listener: DataChangeListener): void { this.notifier.registerDataChangeListener(listener); } unregisterDataChangeListener(listener: DataChangeListener): void { this.notifier.unregisterDataChangeListener(listener); } deleteAllAsReload(): void { this.data.length = 0; this.notifier.notifyReloaded(); } batchDelete(startIndex: number, count: number) { if (startIndex >= 0 && startIndex < this.data.length) { const deleted = this.data.splice(startIndex, count); this.notifier.notifyBatchUpdate([ { type: DataOperationType.DELETE, index: startIndex, count: deleted.length } ]); } } }
- IFetchAgent接口:该实现负责获取数据源项引用的数据。预取器构建器 API 确保数据源项 IDataReferenceItem 和绑定到预取器实例的获取代理具有匹配的类型。
核心代码:
/* * Implementing IFetchAgent for prefetcher using ImageKnife\'s caching capability */ export class ImageKnifeWaterFlowInfoFetchingAgent implements IFetchAgent { private readonly logger = new Logger(\"FetchAgent\"); private readonly fetchToRequestMap: HashMap = new HashMap() /* * Asynchronous prefetching function encapsulated with ImageKnife */ async prefetch(fetchId: FetchId, loadSrc: string): Promise { return new Promise((resolve, reject) => { let imageKnifeOption = new ImageKnifeOption() if (typeof loadSrc == \'string\') { imageKnifeOption.loadSrc = loadSrc; } else { imageKnifeOption = loadSrc; } imageKnifeOption.onLoadListener = { onLoadSuccess: () => { this.fetchToRequestMap.remove(fetchId); resolve(loadSrc); }, onLoadFailed: (err) => { this.fetchToRequestMap.remove(fetchId); reject(err); } } let request = ImageKnife.getInstance().preload(imageKnifeOption); this.fetchToRequestMap.set(fetchId, request); }) } // 实现prefetcher的fetch接口 async fetch(fetchId: FetchId, fetchParameters: FetchParameters): Promise { this.logger.debug(`Fetch ${fetchId}`); let path = await this.prefetch(fetchId, fetchParameters); return path; } // 实现prefetcher的cancel接口 cancel(fetchId: FetchId) { this.logger.debug(`Fetch ${fetchId} cancel`); if (this.fetchToRequestMap.hasKey(fetchId)) { const request = this.fetchToRequestMap.get(fetchId); ImageKnife.getInstance().cancel(request); this.fetchToRequestMap.remove(fetchId); } } }
性能分析
本案例中的页面一屏大概可以加载4至6条数据,每张图片的大小在200-300KB之间。针对使用LazyForeEach的场景,使用prefetch方案前后,快速滑动场景下的白块的数量效果对比如下:
使用prefetch方案前
使用prefetch方案后

在快速滑动的场景下,因为动态预加载能取消对快速划过的图片的数据请求,节省了大量的网络资源,从而减少了白块的数量。