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