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 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 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 184 185图4 优化后使用本地缓存 186 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 341 342优化后使用本地缓存(通过PersistentStorage获取地址数据)的测试步骤:在使用getCurrentLocation获取地址数据后退出应用(本例中在getCurrentLocation获取地址数据数据后会保存到本地缓存),再使用Launch工具抓取性能数据。 343 344图6 优化后使用本地缓存 345 346 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 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.INTERNET。 413 414【优化后】:子页面PageOne中需展示一张较大的网络图片,在父组件的aboutToAppear()中提前发起网络请求,并做判断文件是否存在,已下载的不再重复请求,存储在应用沙箱中。当父页面点击按钮跳转子页面PageOne,此时触发pixMap请求读取应用沙箱中已缓存解码的网络图片并存储在LocalStorage中,通过在子页面的Image中传入被@StorageLink修饰的变量ImageData进行数据刷新,图片送显。 415 416图2 使用预下载的方式,由开发者灵活地处理网络图片,减少白块出现时长。 417 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 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希望通过本文的学习,开发者可以掌握合理使用缓存方法,提升用户体验。