• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 合理使用缓存提升性能
2
3## 简介
4
5随着应用功能的日益丰富与复杂化,数据加载效率成为了衡量应用性能的重要指标。不合理的加载策略往往导致用户面临长时间的等待,这不仅损害了用户体验,还可能引发用户流失。因此,合理运用缓存技术变得尤为重要。
6系统提供了[Preferences](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/data-persistence-by-preferences-V5)、[数据库](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/data-persistence-by-rdb-store-V5)、[文件](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-file-fs-V5)、[AppStorage](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-appstorage-V5)等缓存方式,开发者可以对应用数据先进行缓存,再次加载数据时优先展示缓存数据,减少加载时间,从而提升用户体验。
7本文将介绍以下内容,来帮助开发者通过缓存技术提升应用的冷启动速度、预下载网络图片减少Image白块时长,避免卡顿感:
8
9- [冷启动首页时,缓存网络数据](#场景1缓存网络数据)。
10- [冷启动首页时,缓存地址数据](#场景2缓存地址数据)。
11- [预下载网络图片数据](#场景3预下载图片数据)。
12
13## 识别使用缓存的场景
14
151. 当应用冷启动过程中,应用的首页数据如果依赖于网络请求获取相应数据。可通过[缓存网络数据](#场景1缓存网络数据),从而避免在页面冷启动过程中出现较长时间的白屏或白块现象,提升冷启动速度。
162. 当需要应用在冷启动时即时加载首页地址数据,可通过[缓存地址数据](#场景2缓存地址数据),使用缓存减少首次数据加载展示时间,提升冷启动速度。
173. 当子页面需要加载很大的网络图片时,可以在父页面提前[预下载图片数据](#场景3预下载图片数据)到应用沙箱中,子组件加载时从沙箱中读取,减少Image白块出现时长。
18
19## 冷启动首页时常用的缓存使用流程
20图1 冷启动首页中三种常用的缓存使用流程
21
22![reasonable_using_cache_improve_performance_flow_chart](./figures/reasonable_using_cache_improve_performance_flow_chart.png)
23
24图1是三种常用的缓存使用流程。常用流程1的详细过程如下:
25
261.应用冷启动时,读取缓存。
27
282.判断是否有缓存数据。
29
303.如果本地没有缓存数据,则需要通过网络、位置服务等方式请求相应数据,然后把数据刷新到首页,同时异步更新缓存数据。
31
324.如果本地有缓存数据,则把缓存数据先刷新到应用首页,然后异步请求数据进行页面二刷,并更新缓存数据。
33
34常用流程2和1的过程类似,只是常用流程2中省略了异步请求数据进行页面二刷并更新缓存的步骤。而常用流程3和2相比,常用流程3只是在本地有缓存数据时,增加了对缓存数据是否失效的处理。如果缓存数据没有失效,则把缓存数据刷新到应用首页。如果缓存数据已经失效,则需要重新请求数据,然后刷新到首页并更新缓存。
35
36>**说明:**
37>
38> 上述缓存使用流程仅为开发者提供参考,实际开发中需结合具体业务场景与需求进行灵活的调整与优化。
39
40
41## 优化示例
42
43### 场景1缓存网络数据
44#### 使用场景
45在应用启动过程中,开发者往往会遇到冷启动完成时延长的问题。这是由于大部分应用的首页数据依赖于网络请求或定位服务等方式来获取相应数据。如果网络、位置服务等信号差,就会导致应用请求网络和位置数据耗时变长,从而在页面冷启动过程中出现较长时间的白屏或白块现象。
46因此可以使用本地缓存首页网络数据解决较长时间的白屏或白块问题。
47
48图2 使用本地缓存首页数据流程图
49
50![reasonable_using_cache_improve_performance_network_flow_chart](./figures/reasonable_using_cache_improve_performance_network_flow_chart.png)
51
52图2是使用本地缓存首页数据的流程图。使用本地缓存优先展示冷启动首页数据,可以减少首帧展示完成时延,减少用户可见白屏或白块时间,提升用户的冷启动体验。
53
54>**说明:**
55>
56> 应用需根据自身对于数据的时效性要求,来决定是否使用缓存数据。例如时效性要求为一天时,一天前保存的缓存数据就不适合进行展示,需从网络获取新数据进行展示,并更新本地缓存数据。
57
58#### 场景示例
59下面是一个缓存网络数据的场景示例。示例中应用首页需展示一张从网站获取的图片信息,在aboutToAppear()中发起网络请求,待数据返回解析后展示在首页上。之后将图片信息缓存至本地应用沙箱内,再次冷启动时首先从沙箱内获取图片信息。若存在,即可解析并展示,在网络请求返回时再次更新图片信息。 以下为关键示例代码。
60
61```typescript
62import { http } from '@kit.NetworkKit';
63import { image } from '@kit.ImageKit';
64import { BusinessError } from '@kit.BasicServicesKit';
65import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
66import { fileIo as fs } from '@kit.CoreFileKit';
67
68const PERMISSIONS: Array<Permissions> = [
69  'ohos.permission.READ_MEDIA',
70  'ohos.permission.WRITE_MEDIA'
71];
72AppStorage.link('net_picture');
73PersistentStorage.persistProp('net_picture', '');
74
75@Entry
76@Component
77struct Index {
78  @State image: PixelMap | undefined = undefined;
79  @State imageBuffer: ArrayBuffer | undefined = undefined; // 图片ArrayBuffer
80
81  /**
82   * 通过http的request方法从网络下载图片资源
83   */
84  async getPicture() {
85    http.createHttp()
86      .request('https://www.example1.com/POST?e=f&g=h',
87        (error: BusinessError, data: http.HttpResponse) => {
88          if (error) {
89            return;
90          }
91          // 判断网络获取到的资源是否为ArrayBuffer类型
92          if (data.result instanceof ArrayBuffer) {
93            this.imageBuffer = data.result as ArrayBuffer;
94          }
95          this.transcodePixelMap(data);
96        }
97      )
98  }
99
100  /**
101   * 使用createPixelMap将ArrayBuffer类型的图片装换为PixelMap类型
102   * @param data:网络获取到的资源
103   */
104  transcodePixelMap(data: http.HttpResponse) {
105    if (http.ResponseCode.OK === data.responseCode) {
106      const imageData: ArrayBuffer = data.result as ArrayBuffer;
107      // 通过ArrayBuffer创建图片源实例。
108      const imageSource: image.ImageSource = image.createImageSource(imageData);
109      const options: image.InitializationOptions = {
110        'alphaType': 0, // 透明度
111        'editable': false, // 是否可编辑
112        'pixelFormat': 3, // 像素格式
113        'scaleMode': 1, // 缩略值
114        'size': { height: 100, width: 100 }
115      }; // 创建图片大小
116
117      // 通过属性创建PixelMap
118      imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
119        this.image = pixelMap;
120        setTimeout(() => {
121          if (this.imageBuffer !== undefined) {
122            this.saveImage(this.imageBuffer);
123          }
124        }, 0)
125      });
126    }
127  }
128
129  /**
130   * 保存ArrayBuffer到沙箱路径
131   * @param buffer:图片ArrayBuffer
132   * @returns
133   */
134  async saveImage(buffer: ArrayBuffer | string): Promise<void> {
135    const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
136    const filePath: string = context.cacheDir + '/test.jpg';
137    AppStorage.set('net_picture', filePath);
138    const file = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
139    await fs.write(file.fd, buffer);
140    await fs.close(file.fd);
141  }
142
143  async useCachePic(): Promise<void> {
144    if (AppStorage.get('net_picture') !== '') {
145      // 获取图片的ArrayBuffer
146      const imageSource: image.ImageSource = image.createImageSource(AppStorage.get('net_picture'));
147      const options: image.InitializationOptions = {
148        'alphaType': 0, // 透明度
149        'editable': false, // 是否可编辑
150        'pixelFormat': 3, // 像素格式
151        'scaleMode': 1, // 缩略值
152        'size': { height: 100, width: 100 }
153      };
154      imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
155        this.image = pixelMap;
156      });
157    }
158  }
159
160  async aboutToAppear(): Promise<void> {
161    const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
162    const atManager = abilityAccessCtrl.createAtManager();
163    await atManager.requestPermissionsFromUser(context, PERMISSIONS);
164    this.useCachePic(); // 从本地缓存获取数据
165    this.getPicture(); // 从网络端获取数据
166  }
167
168  build() {
169    Column() {
170      Image(this.image)
171        .objectFit(ImageFit.Contain)
172        .width('50%')
173        .height('50%')
174    }
175  }
176}
177```
178
179#### 性能分析
180
181下面对优化前后启动性能进行对比分析。分析阶段的起点为启动Ability(即H:void OHOS::AppExecFwk::MainThread::HandleLaunchAbility的开始点),阶段终点为应用首次解析Pixelmap(即H:Napi execute, name:CreatePixelMap, traceid:0x0)后的第一个vsync(即H:ReceiveVsync dataCount: 24bytes now:timestamp expectedEnd:timestamp vsyncId:int的开始点)。
182
183图3 优化前未使用本地缓存
184![reasonable_using_cache_improve_performance_network_use_api](./figures/reasonable_using_cache_improve_performance_network_use_api.png)
185
186图4 优化后使用本地缓存
187![reasonable_using_cache_improve_performance_network_use_cache](./figures/reasonable_using_cache_improve_performance_network_use_cache.png)
188
189图3是优化前未使用本地缓存(从网络端获取数据)的耗时,图4是优化后使用本地缓存的耗时,对比数据如下(性能耗时数据因设备版本环境而异,以实测为准):
190
191#### 性能对比
192
193| 方案           |  阶段时长(毫秒)  |
194|--------------|:----------:|
195| (优化前)未使用本地缓存 |   641.8    |
196| (优化后)使用本地缓存   |    68.9    |
197
198可以看到在使用本地缓存后,应用冷启动时从Ability启动到图片显示的阶段耗时明显减少。
199
200### 场景2缓存地址数据
201
202#### 使用场景
203如果应用每次冷启动都先通过[getCurrentLocation](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-geolocationmanager-V5#geolocationmanagergetcurrentlocation)获取位置数据,特别是在信号较弱的区域,这可能导致显著的延迟,迫使用户等待较长时间才能获取到所需的位置信息,从而极大地影响了应用的冷启动体验。
204针对上述问题,下面将通过使用缓存减少首次数据加载展示时间,优化应用启动性能,为开发者优化应用性能提供参考。
205
206下面是一个使用[PersistentStorage(持久化存储UI状态)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-persiststorage-V5)缓存地址数据的场景示例。主要步骤如下:
207
2081.通过persistProp初始化PersistentStorage。
209
2102.创建状态变量@StorageLink(MYLOCATION) myLocation,和AppStorage中MYLOCATION双向绑定。
211
2123.应用冷启动时,先判断缓存AppStorage里MYLOCATION值是否为空(UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问)。
213
2144.如果缓存为空,则从getCurrentLocation获取地址数据,并加载到页面,同时保存到缓存。如果缓存不为空,则直接从缓存获取地址数据,并加载到页面。
215
216>**说明:**
217>
218> 为了方便对比性能差异,本例中未做缓存数据是否失效和页面二刷的业务处理。实际业务开发中冷启动时虽然是优先从缓存获取地址数据进行刷新,但是后面还需要再使用getCurrentLocation获取最新地址数据进行页面二刷,以确保地址数据的准确性。
219
220#### 场景示例
221
222```typescript
223import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'; // 程序访问控制管理模块
224import { BusinessError } from '@kit.BasicServicesKit';
225import { hilog, hiTraceMeter } from '@kit.PerformanceAnalysisKit'; // 性能打点模块
226import { geoLocationManager } from '@kit.LocationKit'; // 位置服务模块。需要在module.json5中配置ohos.permission.APPROXIMATELY_LOCATION权限。
227
228// 写入与读取缓存位置数据的key值
229const MYLOCATION = 'myLocation';
230// 定义获取模糊位置的权限
231const PERMISSIONS: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION'];
232// 初始化PersistentStorage。PersistentStorage用于持久化存储选定的AppStorage属性
233PersistentStorage.persistProp(MYLOCATION, '');
234
235@Entry
236@Component
237struct Index {
238  // 创建状态变量@StorageLink(MYLOCATION) myLocation,和AppStorage中MYLOCATION双向绑定
239  @StorageLink(MYLOCATION) myLocation: string = '';
240  // 获取上下文信息
241  private context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
242
243  aboutToAppear() {
244    // ApiDataTime表示从getCurrentLocation接口获取位置信息的性能打点起始位置。
245    hiTraceMeter.startTrace("ApiDataTime", 1);
246    // CacheDataTime表示从AppStorage缓存中获取位置信息的性能打点起始位置。
247    hiTraceMeter.startTrace("CacheDataTime", 1);
248    // 从AppStorage缓存中获取位置信息
249    let cacheData = AppStorage.get<string>(MYLOCATION);
250    // 缓存中如果有位置信息,则直接从缓存获取位置信息。如果没有,则从getCurrentLocation接口获取位置信息。
251    if (cacheData !== '') {
252      // 缓存中有位置信息,则从缓存中直接获取位置信息,并结束性能打点
253      hiTraceMeter.finishTrace("CacheDataTime", 1);
254      this.getUIContext().showAlertDialog({
255        message: 'AppStorage:' + cacheData,
256        alignment: DialogAlignment.Center
257      });
258    } else {
259      // 缓存中没有位置信息,则从接口获取位置信息
260      this.apiGetLocation(PERMISSIONS, this.context);
261    }
262  }
263
264  /**
265   * 从getCurrentLocation接口获取位置信息。用户需要先授权。
266   */
267  apiGetLocation(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
268    // 获取访问控制模块对象
269    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
270    // 拉起弹框请求用户授权。requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
271    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
272      // 获取相应请求权限的结果。 0表示已授权,否则表示未授权
273      let grantStatus: Array<number> = data.authResults;
274      let length: number = grantStatus.length;
275      for (let i = 0; i < length; i++) {
276        // 如果用户已授权模糊位置的权限,则调用getCurrentLocation获取位置信息,并保存到AppStorage
277        if (data.permissions[i] === 'ohos.permission.APPROXIMATELY_LOCATION' && grantStatus[i] === 0) {
278          // 设置位置请求参数
279          let requestInfo: geoLocationManager.CurrentLocationRequest = {
280            'priority': geoLocationManager.LocationRequestPriority.FIRST_FIX, // 设置优先级信息。FIRST_FIX表示快速获取位置优先,如果应用希望快速拿到一个位置,可以将优先级设置为该字段。
281            'scenario': geoLocationManager.LocationRequestScenario.UNSET // 设置场景信息。UNSET表示未设置场景信息。当scenario取值为UNSET时,priority参数生效,否则priority参数不生效;
282          };
283          try {
284            // 获取当前位置
285            geoLocationManager.getCurrentLocation(requestInfo).then((result) => {
286              // 获取位置信息后,结束性能打点
287              hiTraceMeter.finishTrace("ApiDataTime", 1);
288              let locationData = JSON.stringify(result);
289              // 保存到本地缓存
290              AppStorage.setOrCreate(MYLOCATION, JSON.stringify(locationData));
291              this.getUIContext().showAlertDialog({
292                message: 'getCurrentLocation:' + locationData,
293                alignment: DialogAlignment.Center
294              });
295            })
296              .catch((error: BusinessError) => {
297                hilog.error(0x0000, "UseCacheInsteadAddressInquiry", `getCurrentLocation: error= ${error}`);
298              });
299          } catch (err) {
300            hilog.error(0x0000, "UseCacheInsteadAddressInquiry", `err: ${err}`);
301          }
302        } else {
303          // 如果用户未授权,提示用户授权。
304          this.getUIContext().showAlertDialog({
305            message: '用户未授权,请到系统设置中打开应用的位置权限后再试。',
306            alignment: DialogAlignment.Center
307          });
308          return;
309        }
310      }
311    }).catch((err: BusinessError) => {
312      hilog.error(0x0000, "UseCacheInsteadAddressInquiry", `failed to request permissions from user. Code is ${err.code} , message is ${err.message}`);
313    })
314  }
315
316  build() {
317    Column() {
318      Button('clear cache').onClick(() => {
319        // 清除AppStorage缓存中的位置信息
320        this.myLocation = '';
321        this.getUIContext().showAlertDialog({
322          message: 'cache cleared',
323          alignment: DialogAlignment.Center
324        });
325      })
326    }
327    .height('100%')
328    .width('100%')
329  }
330}
331```
332
333#### 性能分析
334
335下面使用DevEco Studio内置的Profiler中的启动分析工具Launch,对使用getCurrentLocation获取地址数据及使用缓存获取地址数据的冷启动性能进行对比分析。本例中通过在aboutToAppear进行起始位置的[性能打点](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-hitracemeter-V5),然后在使用本地缓存和使用getCurrentLocation获取到地址数据的位置分别进行结束位置的性能打点来分析两者的性能差异。对比性能前,需要先打开一次应用页面,在弹出位置信息授权弹窗时选择允许授权的选项。
336
337优化前未使用本地缓存(通过getCurrentLocation获取地址数据)的测试步骤:先打开示例页面,点击'clear cache'按钮(清除本地位置信息的缓存)后退出应用,再使用Launch抓取性能数据。
338
339图5 优化前未使用本地缓存
340
341![reasonable_using_cache_improve_performance_use_api](./figures/reasonable_using_cache_improve_performance_use_api.png)
342
343优化后使用本地缓存(通过PersistentStorage获取地址数据)的测试步骤:在使用getCurrentLocation获取地址数据后退出应用(本例中在getCurrentLocation获取地址数据数据后会保存到本地缓存),再使用Launch工具抓取性能数据。
344
345图6 优化后使用本地缓存
346
347![reasonable_using_cache_improve_performance_use_cache](./figures/reasonable_using_cache_improve_performance_use_cache.png)
348
349图5是优化前未使用本地缓存(从getCurrentLocation获取地址数据)的耗时,图6是优化后使用本地缓存(从PersistentStorage获取地址数据)的耗时,对比数据如下(性能耗时数据因设备版本环境而异,以实测为准):
350
351#### 性能对比
352| 方案                     | 阶段时长 |
353| ------------------------ | :------: |
354| (优化前)未使用本地缓存 |   46ms   |
355| (优化后)使用本地缓存   |   19μs   |
356
357由此可见,在冷启动首页需要加载地址数据的场景中,先采用本地缓存策略获取地址数据相比调用getCurrentLocation接口,能显著缩短地址数据的获取时间,减少用户等待,提升冷启动完成时延性能与用户体验。
358
359### 场景3预下载图片数据
360#### 原理介绍
361在通过Image组件加载网络图片时,通常会经历四个关键阶段:组件创建、图片资源下载、图片解码和刷新。当加载的图片资源过大时,Image组件会在图片数据下载和解码完成后才刷新图片。这一过程中,由于图片下载较耗时,未成功加载的图片常常表现为空白或占位图(一般为白色或淡色),这可能引发“Image 白块”现象。为了提升用户体验并提高性能,应尽量避免这种情况。
362图1 Image加载网络图片两种方式对比
363![reasonable_using_cache_improve_performance_use_preRequest](./figures/reasonable_using_cache_improve_performance_use_preRequest.png)
364
365为了减少白块的出现,开发者可以采用预下载的方式,可以将网络图片通过应用沙箱的方式进行提前缓存,将图片下载解码提前到组件创建之前执行,当Image组件加载时从应用沙箱中获取缓存数据。非首次请求时会判断应用沙箱里是否存在资源,如存在直接从缓存里获取,不再重复下载,减少Image加载大的网络图片时白屏或白块出现时长较长的问题,提升用户体验。
366>**说明:**
367>
368> 1. 开发者在使用Image加载较大的网络图片时,网络下载推荐使用HTTP工具提前预下载。
369> 2. 在预下载之后,开发者可根据业务自行选择数据处理方式,如将预下载后得到的ArrayBuffer转成BASE64、使用应用沙箱提前缓存、直接转PixelMap、或是业务上自行处理ArrayBuffer等多种方式灵活处理数据后,传给Image组件。
370
371#### 使用场景
372当子页面需要加载很大的网络图片时,可以在父页面提前将网络数据预下载到应用沙箱中,子组件加载时从沙箱中读取,减少白块出现时长。
373
374#### 场景示例
375开发者使用Navigation组件时,通常会在主页引入子页面组件,在按钮中添加方法实现跳转子页面组件。当子页面中需展示一张较大的网络图片时,而Image未设置占位图时,会出现点击按钮后,子组件的Image组件位置出现长时间的Image白块现象。
376
377本文将以应用沙箱提前缓存举例,给出减少Image白块出现时长的一种优化方案。
378
379【优化前】:使用Image组件直接加载网络地址。
380
381以下为部分示例代码:
382```typescript
383@Builder
384export function PageOneBuilder(name: string) {
385  PageOne()
386}
387
388@Component
389export struct PageOne {
390  pageInfo: NavPathStack = new NavPathStack();
391  @State name: string = 'pageOne';
392
393  build() {
394    NavDestination() {
395      Row() {
396        // 不推荐用法:使用Image直接加载网络图片的方式,受到图片下载与解析的耗时影响,极易出现白块。
397        Image("https://www.example.com/xxx.png") // 此处请填写一个具体的网络图片地址。
398          .objectFit(ImageFit.Auto)
399          .width('100%')
400          .height('100%')
401      }
402      .width('100%')
403      .height('100%')
404      .justifyContent(FlexAlign.Center)
405    }
406    .title(this.name)
407  }
408}
409```
410>**说明:**
411>
412> 1. 使用Image直接加载网络图片时,可以使用.alt()的方式,在网络图片加载成功前使用占位图,避免白块出现时长过长,优化用户体验。
413> 2. 使用网络图片时,需要申请权限ohos.permission.INTERNET414
415【优化后】:子页面PageOne中需展示一张较大的网络图片,在父组件的aboutToAppear()中提前发起网络请求,并做判断文件是否存在,已下载的不再重复请求,存储在应用沙箱中。当父页面点击按钮跳转子页面PageOne,此时触发pixMap请求读取应用沙箱中已缓存解码的网络图片并存储在LocalStorage中,通过在子页面的Image中传入被@StorageLink修饰的变量ImageData进行数据刷新,图片送显。
416
417图2 使用预下载的方式,由开发者灵活地处理网络图片,减少白块出现时长。
418![reasonable_using_cache_improve_performance_use_preRequest2](./figures/reasonable_using_cache_improve_performance_use_preRequest2.png)
419以下为关键示例代码:
420
4211. 在父组件里aboutToAppear()中提前发起网络请求,当父页面点击按钮跳转子页面PageOne,此时触发pixMap请求读取应用沙箱中已缓存解码的网络图片并存储在localStorage中。非首次点击时,不再重复调用getPixMap(),避免每次点击都从沙箱里读取文件。
422```typescript
423import { fileIo as fs } from '@kit.CoreFileKit';
424import { image } from '@kit.ImageKit';
425import { common } from '@kit.AbilityKit';
426import { httpRequest } from '../utils/NetRequest';
427
428let para: Record<string, PixelMap | undefined> = { 'imageData': undefined };
429let localStorage: LocalStorage = new LocalStorage(para);
430
431@Entry(localStorage)
432@Component
433struct MainPage {
434  @State childNavStack: NavPathStack = new NavPathStack();
435  @LocalStorageLink('imageData') imageData: PixelMap | undefined = undefined;
436  @State fileUrl: string = '';
437
438  getPixMap() { // 从应用沙箱里读取文件
439    try {
440      let file = fs.openSync(this.fileUrl, fs.OpenMode.READ_WRITE); // 以同步方法打开文件
441      const imageSource: image.ImageSource = image.createImageSource(file.fd);
442      const options: image.InitializationOptions = {
443        'alphaType': 0, // 透明度
444        'editable': false, // 是否可编辑
445        'pixelFormat': 3, // 像素格式
446        'scaleMode': 1, // 缩略值
447        'size': { height: 100, width: 100 }
448      };
449      fs.close(file);
450      imageSource.createPixelMap(options).then((pixelMap: PixelMap) => {
451        this.imageData = pixelMap;
452      });
453    } catch (e) {
454      console.error('资源加载错误,文件或不存在!');
455    }
456  }
457
458  aboutToAppear(): void {
459    // 获取应用文件路径
460    let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
461    let filesDir = context.filesDir;
462    this.fileUrl = filesDir + '/xxx.png'; // 当使用实际网络地址时,需填入实际地址的后缀。
463    httpRequest(context); // 在父组件提前发起网络请求
464  }
465
466  build() {
467    Navigation(this.childNavStack) {
468      Column() {
469        Button('push Path to pageOne', { stateEffect: true, type: ButtonType.Capsule })
470          .width('80%')
471          .height(40)
472          .margin({ bottom: '36vp' })
473          .onClick(() => {
474            if (!localStorage.get('imageData')) { // 非首次点击,不再重复调用getPixMap(),避免每次点击都从沙箱里读取文件。
475              this.getPixMap();
476            }
477            this.childNavStack.pushPath({ name: 'pageOne' });
478          })
479      }
480      .width('100%')
481        .height('100%')
482        .justifyContent(FlexAlign.End)
483    }
484    .backgroundColor(Color.Transparent)
485      .title('ParentNavigation')
486  }
487}
488```
4892. 在NetRequest.ets中定义网络请求httpRequest(),通过fs.access()检查文件是否存在,当文件存在时不再重复请求,并写入沙箱中。
490```typescript
491import { http } from '@kit.NetworkKit';
492import { BusinessError } from '@kit.BasicServicesKit';
493import { fileIo as fs } from '@kit.CoreFileKit';
494import { common } from '@kit.AbilityKit';
495
496export async function httpRequest(context: common.UIAbilityContext) {
497  // 获取应用文件路径
498  let filesDir = context.filesDir;
499  let fileUrl = filesDir + '/xxx.png'; // 当使用实际网络地址时,需填入实际地址的后缀。
500  fs.access(fileUrl, fs.AccessModeType.READ).then((res) => { // 检查文件是否存在
501    if (!res) { // 如沙箱里不存在地址,重新请求网络图片资源
502      http.createHttp()
503        .request('https://www.example.com/xxx.png', // 此处请填写一个具体的网络图片地址。
504          (error: BusinessError, data: http.HttpResponse) => {
505            if (error) {
506              // 下载失败时不执行后续逻辑
507              return;
508            }
509            // 处理网络请求返回的数据
510            if (http.ResponseCode.OK === data.responseCode) {
511              const imageData: ArrayBuffer = data.result as ArrayBuffer;
512              // 保存图片到应用沙箱
513              readWriteFileWithStream(fileUrl, imageData);
514            }
515          }
516        )
517    }
518  })
519}
520
521// 写入到沙箱
522async function readWriteFileWithStream(fileUrl: string, imageData: ArrayBuffer): Promise<void> {
523  let outputStream = fs.createStreamSync(fileUrl, 'w+');
524  await outputStream.write(imageData);
525  outputStream.closeSync();
526}
527```
5283. 在子组件中通过在子页面的Image中传入被@StorageLink修饰的变量ImageData进行数据刷新,图片送显。
529```typescript
530@Builder
531export function PageOneBuilder(name: string,param: Object) {
532  PageOne()
533}
534
535@Component
536export struct PageOne {
537  pageInfo: NavPathStack = new NavPathStack();
538  @State name: string = 'pageOne';
539  @LocalStorageLink('imageData') imageData: PixelMap | undefined = undefined;
540
541  build() {
542    NavDestination() {
543      Row() {
544        Image(this.imageData) // 正例:此时Image拿到已提前加载好的网络图片,减少了白块出现时长
545          .objectFit(ImageFit.Auto)
546          .width('100%')
547          .height('100%')
548      }
549      .width('100%')
550        .height('100%')
551        .justifyContent(FlexAlign.Center)
552    }
553    .title(this.name)
554  }
555}
556```
557#### 性能分析
558下面,使用trace对优化前后性能进行对比分析。
559
560【优化前】
561
562分析阶段的起点为父页面点击按钮开始计时即trace的H:DispatchTouchEvent,结束点为子页面图片渲染的首帧出现即H:CreateImagePixelMap标签后的第一个Vsync,记录白块出现时间为1.3s,其中以H:HttpRequestInner的标签起始为起点到H:DownloadImageSuccess标签结束为终点记录时间,即为网络下载耗时1.2s,因此使用Image直接加载网络图片时,出现长时间Image白块,其原因是需要等待网络下载资源完成。
563
564图3 直接使用Image加载网络数据
565![reasonable_using_cache_improve_performance_use_preRequest3](./figures/reasonable_using_cache_improve_performance_use_preRequest3.png)
566
567【优化后】
568
569分析阶段的起点为父页面点击按钮开始计时即trace的H:DispatchTouchEvent,结束点为子页面图片渲染的首帧出现即H:CreateImagePixelMap标签后的第一个Vsync,记录白块出现时间为32.6ms,其中记录H:HttpRequestInner的标签耗时即为提前网络下载的耗时1.16s,对比白块时长可知提前预下载可以减少白块出现时长。
570
571图4 使用预下载的方式
572![reasonable_using_cache_improve_performance_use_preRequest4](./figures/reasonable_using_cache_improve_performance_use_preRequest4.png)
573
574>**说明:**
575>
576> 网络下载耗时实际受到网络波动影响,优化前后的网络下载耗时数据总体差异在1s内,提供的性能数值仅供参考。
577
578#### 效果对比
579|                                                     (优化前)<br/>直接使用Image加载网络数据,未使用预下载                                                      |                                                                (优化后)使用预下载                                                                 |
580|:-----------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------:|
581|  ![reasonable_using_cache_improve_performance_use_preRequest5](./figures/reasonable_using_cache_improve_performance_use_preRequest5.gif)  |  ![reasonable_using_cache_improve_performance_use_preRequest6](./figures/reasonable_using_cache_improve_performance_use_preRequest6.gif)  |
582
583#### 性能对比
584对比数据如下:
585
586| 方案                          | 白块出现时长(毫秒)       | 白块出现时长        |
587|:----------------------------|:-----------------|:--------------|
588| (优化前)直接使用Image加载网络数据,未使用预下载 | 1300             | 图片位置白块出现时间较长。 |
589| (优化后)使用预下载                  | 32.6             | 图片位置白块出现时间较短。 |
590
591>**说明:**
592>
593> 测试数据仅限于示例程序,不同设备特性和具体应用场景的多样性,所获得的性能数据存在差异,提供的数值仅供参考。
594
595由此可见,加载网络图片时,使用预下载,提前处理网络请求并从应用沙箱中读取缓存数据的方式,可以减少用户可见Image白屏或白块出现时长,提升用户体验。
596## 总结
597
598本文通过介绍了如何识别使用缓存场景以及优化方法。
599- 提升应用冷启动速度:将频繁请求的网络数据或位置信息等缓存起来,可以在下次启动时优先加载缓存数据,避免网络延迟或位置服务信号差导致的白屏或白块现象。
600- 避免Image加载网络图片长时间白块问题:加载网络图片时,使用预下载,提前处理网络请求并从应用沙箱中读取缓存数据的方式,可以减少用户可见Image白屏或白块出现时长。
601
602希望通过本文的学习,开发者可以掌握合理使用缓存方法,提升用户体验。