往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)

✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✏️ 记录一场鸿蒙开发岗位面试经历~

✏️ 持续更新中……


概述

在通过Image组件加载网络图片时,通常会经历四个关键阶段:组件创建、图片资源下载、图片解码和刷新。当加载的图片资源过大时,Image组件会在图片数据下载和解码完成后才刷新图片。这一过程中,由于图片下载较耗时,未成功加载的图片常常表现为空白或占位图(一般为白色或淡色),这可能引发“Image 白块”现象。为了提升用户体验并提高性能,应尽量避免这种情况。

图1 Image加载网络图片两种方式对比

为了减少白块的出现,开发者可以采用预下载的方式,可以将网络图片通过 应用沙箱 的方式进行提前缓存,将图片下载解码提前到组件创建之前执行,当Image组件加载时从应用沙箱中获取缓存数据。非首次请求时会判断应用沙箱里是否存在资源,如存在直接从缓存里获取,不再重复下载,减少Image加载大的网络图片时白屏或白块出现时长较长的问题,提升用户体验。

说明
1. 开发者在使用Image加载较大的网络图片时,网络下载推荐使用 HTTP 工具提前预下载。

2. 在预下载之后,开发者可根据业务自行选择数据处理方式,如将预下载后得到的ArrayBuffer转成BASE64、使用应用沙箱提前缓存、直接转PixelMap、或是业务上自行处理ArrayBuffer等多种方式灵活处理数据后,传给Image组件。

使用场景

当子页面需要加载很大的网络图片时,可以在父页面提前将网络数据预下载到应用沙箱中,子组件加载时从沙箱中读取,减少白块出现时长。

场景案例

开发者使用Navigation组件时,通常会在主页引入子页面组件,在按钮中添加方法实现跳转子页面组件。当子页面中需展示一张较大的网络图片时,而Image未设置占位图时,会出现点击按钮后,子组件的Image组件位置出现长时间的Image白块现象。

本文将以应用沙箱提前缓存举例,给出减少Image白块出现时长的一种优化方案。

【优化前】:使用Image组件直接加载网络地址

以下为部分示例代码:

@Builder
export function PageOneBuilder(name: string) {
  PageOne()
}

@Component
export struct PageOne {
  pageInfo: NavPathStack = new NavPathStack();
  @State name: string = 'pageOne';

  build() {
    NavDestination() {
      Row() {
        // 不推荐用法:使用Image直接加载网络图片的方式,受到图片下载与解析的耗时影响,极易出现白块。
        Image("https://www.example.com/xxx.png") // 此处请填写一个具体的网络图片地址。
          .objectFit(ImageFit.Auto)
          .width('100%')
          .height('100%')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
    }
    .title(this.name)
  }
}

说明

  • 使用Image直接加载网络图片时,可以使用.alt()的方式,在网络图片加载成功前使用占位图,避免白块出现时长过长,优化用户体验。
  • 使用网络图片时,需要申请权限ohos.permission.INTERNET。

【优化后】:子页面PageOne中需展示一张较大的网络图片,在父组件的aboutToAppear()中提前发起网络请求,并做判断文件是否存在,已下载的不再重复请求,存储在应用沙箱中。当父页面点击按钮跳转子页面PageOne,此时触发pixMap请求读取应用沙箱中已缓存解码的网络图片并存储在LocalStorage中,通过在子页面的Image中传入被@StorageLink修饰的变量ImageData进行数据刷新,图片送显。

图2 使用预下载的方式,由开发者灵活地处理网络图片,减少白块出现时长。

以下为关键示例代码:

  1. 在父组件里aboutToAppear()中提前发起网络请求,当父页面点击按钮跳转子页面PageOne,此时触发pixMap请求读取应用沙箱中已缓存解码的网络图片并存储在localStorage中。非首次点击时,不再重复调用getPixMap(),避免每次点击都从沙箱里读取文件。
import { fileIo as fs } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
import { httpRequest } from '../utils/NetRequest';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
let fileUrl = filesDir + '/xxx.png'; // 当使用实际网络地址时,需填入实际地址的后缀。
let para: Record<string, PixelMap | undefined> = { 'imageData': undefined };
let localStorage: LocalStorage = new LocalStorage(para);

@Entry(localStorage)
@Component
struct MainPage {
  @State childNavStack: NavPathStack = new NavPathStack();
  @LocalStorageLink('imageData') imageData: PixelMap | undefined = undefined;

  getPixMap() { // 从应用沙箱里读取文件
    try {
      let file = fs.openSync(fileUrl, fs.OpenMode.READ_WRITE); // 以同步方法打开文件
      const imageSource: image.ImageSource = image.createImageSource(file.fd);
      const options: image.InitializationOptions = {
        'alphaType': 0, // 透明度
        'editable': false, // 是否可编辑
        'pixelFormat': 3, // 像素格式
        'scaleMode': 1, // 缩略值
        'size': { height: 100, width: 100 }
      };
      fs.close(file);
      imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
        this.imageData = pixelMap;
      });
    } catch (e) {
      console.error('资源加载错误,文件或不存在!');
    }
  }

  aboutToAppear(): void {
    httpRequest(); // 在父组件提前发起网络请求
  }

  build() {
    Navigation(this.childNavStack) {
      Column() {
        Button('push Path to pageOne', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin({ bottom: '36vp' })
          .onClick(() => {
            if (!localStorage.get('imageData')) { // 非首次点击,不再重复调用getPixMap(),避免每次点击都从沙箱里读取文件。
              this.getPixMap();
            }
            this.childNavStack.pushPath({ name: 'pageOne' });
          })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.End)
    }
    .backgroundColor(Color.Transparent)
    .title('ParentNavigation')
  }
}
  1. 在NetRequest.ets中定义网络请求httpRequest(),通过fs.access()检查文件是否存在,当文件存在时不再重复请求,并写入沙箱中。
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
let fileUrl = filesDir + '/xxx.png'; // 当使用实际网络地址时,需填入实际地址的后缀。

export async function httpRequest() {
  fs.access(fileUrl, fs.AccessModeType.READ).then((res) => { // 检查文件是否存在
    if (!res) { // 如沙箱里不存在地址,重新请求网络图片资源
      http.createHttp()
        .request('https://www.example.com/xxx.png', // 此处请填写一个具体的网络图片地址。
          (error: BusinessError, data: http.HttpResponse) => {
            if (error) {
              // 下载失败时不执行后续逻辑
              return;
            }
            // 处理网络请求返回的数据
            if (http.ResponseCode.OK === data.responseCode) {
              const imageData: ArrayBuffer = data.result as ArrayBuffer;
              // 保存图片到应用沙箱
              readWriteFileWithStream(imageData);
            }
          }
        )
    }
  })
}

// 写入到沙箱
async function readWriteFileWithStream(imageData: ArrayBuffer): Promise<void> {
  let outputStream = fs.createStreamSync(fileUrl, 'w+');
  await outputStream.write(imageData);
  outputStream.closeSync();
}
  1. 在子组件中通过在子页面的Image中传入被@StorageLink修饰的变量ImageData进行数据刷新,图片送显。
@Builder
export function PageOneBuilder(name: string,param: Object) {
  PageOne()
}

@Component
export struct PageOne {
  pageInfo: NavPathStack = new NavPathStack();
  @State name: string = 'pageOne';
  @LocalStorageLink('imageData') imageData: PixelMap | undefined = undefined;

  build() {
    NavDestination() {
      Row() {
        Image(this.imageData) // 正例:此时Image拿到已提前加载好的网络图片,减少了白块出现时长
          .objectFit(ImageFit.Auto)
          .width('100%')
          .height('100%')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
    }
    .title(this.name)
  }
}

性能分析

下面,使用trace对优化前后性能进行对比分析。

【优化前】

分析阶段的起点为父页面点击按钮开始计时即trace的H:DispatchTouchEvent,结束点为子页面图片渲染的首帧出现即H:CreateImagePixelMap标签后的第一个Vsync,记录白块出现时间为1.3s,其中以H:HttpRequestInner的标签起始为起点到H:DownloadImageSuccess标签结束为终点记录时间,即为网络下载耗时1.2s,因此使用Image直接加载网络图片时,出现长时间Image白块,其原因是需要等待网络下载资源完成。

图3 直接使用Image加载网络数据

【优化后】

分析阶段的起点为父页面点击按钮开始计时即trace的H:DispatchTouchEvent,结束点为子页面图片渲染的首帧出现即H:CreateImagePixelMap标签后的第一个Vsync,记录白块出现时间为32.6ms,其中记录H:HttpRequestInner的标签耗时即为提前网络下载的耗时1.16s,对比白块时长可知提前预下载可以减少白块出现时长。

图4 使用预下载的方式

说明
网络下载耗时实际受到网络波动影响,优化前后的网络下载耗时数据总体差异在1s内,提供的性能数值仅供参考。

效果对比

  • (优化前)直接使用Image加载网络数据,未使用预下载

  • (优化后)使用预下载

性能对比

对比数据如下:

方案 白块出现时长(毫秒) 白块出现时长
(优化前)直接使用Image加载网络数据,未使用预下载 1300 图片位置白块出现时间较长
(优化后)使用预下载 32.6 图片位置白块出现时间较短

说明

1.测试数据仅限于示例程序,不同设备特性和具体应用场景的多样性,所获得的性能数据存在差异,提供的数值仅供参考。
2.由于该方案仅将下载解码网络图片的步骤提前,不会影响内存等应用数据。开发者可自行管理解码后的PixelMap,主动实现图片的复用和缓存。

由此可见,加载网络图片时,使用预下载,提前处理网络请求并从应用沙箱中读取缓存数据的方式,可以减少用户可见Image白屏或白块出现时长,提升用户体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐