简介
使用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,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会缓存组件,并在下一次迭代中使用。
详细介绍可参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-rendering-control-repeat-V5
prefetcher原理介绍
prefetch是一种动态预加载技术,通过考虑滚动速度、屏幕上的项目数量等因素,动态的下载或取消下载资源,确保相关资源在需要时能立即显示。
详细介绍可参考:https://ohpm.openharmony.cn/\#/cn/detail/@netteam%2Fprefetcher
页面抛滑白块优化解决方案原理介绍
使用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 < 20) {
this.addItemsCount ++;
setTimeout(() => {
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<WaterFlowInfoItem>(this.items)
.each((obj: RepeatItem<WaterFlowInfoItem>) => {
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<FetchParameters, PathToResultFile> {
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<WaterFlowInfoItem> {
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<WaterFlowInfoItem[]> {
let waterFlowInfoArray: WaterFlowInfo[] =
await this.netWorkUtil.getWaterFlowData(CommonConstants.MOCK_INTERFACE_PATH_NAME, fileName, pageNo, pageSize);
for (let index = 0; index < waterFlowInfoArray.length; index++) {
if (waterFlowInfoArray[index].waterFlowDescription.userName.length > 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<FetchParameters, PathToResultFile> {
private readonly logger = new Logger("FetchAgent");
private readonly fetchToRequestMap: HashMap<FetchId, ImageKnifeRequest> = new HashMap()
/*
* Asynchronous prefetching function encapsulated with ImageKnife
*/
async prefetch(fetchId: FetchId, loadSrc: string): Promise<string> {
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<PathToResultFile> {
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方案后
在快速滑动的场景下,因为动态预加载能取消对快速划过的图片的数据请求,节省了大量的网络资源,从而减少了白块的数量。
示例代码
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。