背景

中国移动云盘(原“和彩云网盘”)是中国移动重磅推出的安全、智能、不限速、移动用户免流的智能云盘,致力于成为5G时代用户个人与家庭的数字资产管理中心,是中国移动继语音、短信、流量后的“第四项基础服务”。

照片、音视频的备份和使用是市面上所有网盘用户的重点体验场景,用户将照片等资源备份到云盘,以节省本地资源存储。批量的图片、音视频缩略图加载,有时因网络原因出现加载缓慢等异常。中国移动云盘技术团队,致力于实现极致流畅的图文加载体验持续优化。

解决方案

以中国移动云盘鸿蒙原生版为例,借助华为Remote Communication Kit提供的智能网络感知服务,中国移动云盘通过三个步骤,提升了用户使用云盘时的加载体验,为用户提供更丝滑的云盘服务。

步骤一:动态预加载机制

以中国移动云盘的相册为例,当用户打开相册时,图文加载区域分为预加载区域、预渲染区域以及可见区域。

首先,智能网络感知服务帮助中国移动云盘优先保证用户可见区域的加载完成,并跟随屏幕滑动动态调整预加载区域,同时通过本地文件缓存替代实时加载的形式,在屏幕滑动时大大降低了图片白块概率;再通过减少预渲染区域预加载的图片数量,降低UI渲染带来的CPU负载开销。

步骤二:动态取消加载机制

当用户快速滑动屏幕时,有时候有的资源已经滑出了屏幕,但网络请求还未到达。中国移动云盘基于智能网络感知服务的网络请求可取消机制,在用户快速滑动时,将滑过屏幕的资源取消加载,降低网络负载。

步骤三:智能网络感知下动态调控预加载区域机制

基于RCP的网络耗时统计能力,可以计算出每个资源请求的平均耗时,感知网络质量并智能调整预加载区域。在网络质量好时,增大预加载区域,网络质量差时,就降低预加载区域。

成果展示

最终,通过中国移动云盘专家与华为网络通信专家联合构建图文资源动态加载,实现了鸿蒙原生版中国移动云盘网络资源如本地资源一样的流畅加载体验。
优化前:有时因网络原因出现加载占位图和加载过程动画。

优化后:随意滑动,图文内容加载非常流畅自然。

开发指南

1、下载并安装:

ohpm install @netteam/prefetcher

有关 OpenHarmony ohpm 环境配置的更多信息,请参阅如何安装 OpenHarmony ohpm 包

2、集成预取器:

在深入探讨细节之前,需要提及两点。

1.预取程序驱动用户代码获取所显示集合中的项目所引用的资源。换句话说,用户代码获取 URL 存储在集合项目中的图像。

2.有两种使用 Prefetcher API 的方法。第一种方法假设保存项目的数据源实现了两种附加方法:prefetch()和cancel() IDataSourcePrefetching。第二种方法(稍后介绍)使用用户注册的回调来执行相同的任务。方法的选择由用户决定,但“基于回调”的方法比“基于预取数据源”的方法更具优势:

更容易组织工作线程上的提取。

支持以数组形式呈现的集合,并因此支持Repeat组件。

通过遵循单一责任原则来更好地组织应用程序代码,因为数据源不负责执行获取。

3、使用“基于预取数据源”的方法

以下是Prefetcher 演示存储库中示例应用程序之一的详细代码片段。该应用程序演示了预取器库的使用。演示应用程序包含大量图像。每幅图像都是在应用程序内合成的彩色矩形。获取操作在主应用程序线程上执行。

import { BasicPrefetcher, IDataSourcePrefetching } from '@netteam/prefetcher';
import { image } from '@kit.ImageKit';
import { create10x10Bitmap, getRandomColor } from 'examples_common'; // Code of these functions is omitted for brevity

const ITEMS_ON_SCREEN = 8;

@Component
export struct PrefetcherDemoComponent {
  private readonly dataSource = new SimulationDataSource(2000, 500);
  private readonly prefetcher = new BasicPrefetcher(this.dataSource);

  build() {
    Column() {
      List() {
        LazyForEach(this.dataSource, (item: PictureItem) => {
          ListItem() {
            PictureItemComponent({ info: item })
              .height(`${100 / ITEMS_ON_SCREEN}%`)
          }
        }, (item: PictureItem) => item.title)
      }
      .onScrollIndex((start: number, end: number) => {
        this.prefetcher.visibleAreaChanged(start, end);
      })
    }
  }
}

@Component
export default struct PictureItemComponent {
  @ObjectLink info: PictureItem;

  build() {
    Row() {
      Image(this.info.imagePixelMap)
        .objectFit(ImageFit.Contain)
        .width('40%')
      Text(this.info.title)
        .width('60%')
    }
  }
}

@Observed
export class PictureItem {
  readonly color: number;
  title: string;
  imagePixelMap: image.PixelMap | undefined;
  key: string;

  constructor(color: number, title: string) {
    this.color = color;
    this.title = title;
    this.key = title;
  }
}

type ItemIndex = number;
type TimerId = number;

class SimulationDataSource implements IDataSourcePrefetching {
  private readonly items: PictureItem[];
  private readonly fetchDelayMs: number;
  private readonly fetches: Map<ItemIndex, TimerId> = new Map();

  constructor(numItems: number, fetchDelayMs: number) {
    this.items = [];
    this.fetchDelayMs = fetchDelayMs;
    for (let i = 0; i < numItems; i++) {
      const item = new PictureItem(getRandomColor(), `Item ${i}`);
      this.items.push(item);
    }
  }

  async prefetch(index: number): Promise<void> {
    const item = this.items[index];
    if (item.imagePixelMap) {
      return;
    }

    // Simulate long running operation
    return new Promise<void>(resolve => {
      const timeoutId = setTimeout(async () => {
        this.fetches.delete(index);
        const bitmap = create10x10Bitmap(item.color);
        const imageSource: image.ImageSource = image.createImageSource(bitmap);
        item.imagePixelMap = await imageSource.createPixelMap();
        resolve();
      }, this.fetchDelayMs);

      this.fetches.set(index, timeoutId);
    });
  }

  cancel(index: number): void {
    const timerId = this.fetches.get(index);
    if (timerId) {
      this.fetches.delete(index);
      clearTimeout(timerId);
    }
  }

  totalCount(): number {
    return this.items.length;
  }

  getData(index: number): PictureItem {
    return this.items[index];
  }

  registerDataChangeListener(_: DataChangeListener): void {}

  unregisterDataChangeListener(_: DataChangeListener): void {}
}

该类PictureItem表示引用需要“下载”才能呈现给用户的数据的项目。获取后,它将获得Image组件的像素图。

是SimulationDataSource与预取器和组件链接的数据源LazyForEach。其单个参数指定列表大小。它负责执行获取操作并模拟获取图像,并附加延迟,可在其构造函数中设置。延迟以毫秒为单位指定。

一旦图片准备好,就会包装每个并允许显示图像PictureItemComponent。PictureItem

探索更多

Remote Communication Kit是HarmonyOS系统提供对HTTP发起数据请求的NAPI封装,通过@hms.collaboration.rcp(简称rcp)模块将相关能力开放给开发者,为开发者提供基于rcp的文件上传与下载的开发实践。

访问Remote Communication Kit华为开发者联盟官网,以及开发者方案示例,了解更多详情开始使用。


HarmonyOS_SDK
596 声望11.7k 粉丝

HarmonyOS SDK通过将HarmonyOS系统级能力对外开放,支撑开发者高效打造更纯净、更智能、更精致、更易用的鸿蒙原生应用,和开发者共同成长。