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 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 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 185 186图4 优化后使用本地缓存 187 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 342 343优化后使用本地缓存(通过PersistentStorage获取地址数据)的测试步骤:在使用getCurrentLocation获取地址数据后退出应用(本例中在getCurrentLocation获取地址数据数据后会保存到本地缓存),再使用Launch工具抓取性能数据。 344 345图6 优化后使用本地缓存 346 347 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 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.INTERNET。 414 415【优化后】:子页面PageOne中需展示一张较大的网络图片,在父组件的aboutToAppear()中提前发起网络请求,并做判断文件是否存在,已下载的不再重复请求,存储在应用沙箱中。当父页面点击按钮跳转子页面PageOne,此时触发pixMap请求读取应用沙箱中已缓存解码的网络图片并存储在LocalStorage中,通过在子页面的Image中传入被@StorageLink修饰的变量ImageData进行数据刷新,图片送显。 416 417图2 使用预下载的方式,由开发者灵活地处理网络图片,减少白块出现时长。 418 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 566 567【优化后】 568 569分析阶段的起点为父页面点击按钮开始计时即trace的H:DispatchTouchEvent,结束点为子页面图片渲染的首帧出现即H:CreateImagePixelMap标签后的第一个Vsync,记录白块出现时间为32.6ms,其中记录H:HttpRequestInner的标签耗时即为提前网络下载的耗时1.16s,对比白块时长可知提前预下载可以减少白块出现时长。 570 571图4 使用预下载的方式 572 573 574>**说明:** 575> 576> 网络下载耗时实际受到网络波动影响,优化前后的网络下载耗时数据总体差异在1s内,提供的性能数值仅供参考。 577 578#### 效果对比 579| (优化前)<br/>直接使用Image加载网络数据,未使用预下载 | (优化后)使用预下载 | 580|:-----------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------:| 581|  |  | 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希望通过本文的学习,开发者可以掌握合理使用缓存方法,提升用户体验。