1# PersistentStorage:持久化存储UI状态 2 3 4前两个小节介绍的LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。 5 6 7PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。 8 9 10PersistentStorage提供状态变量持久化的能力,但是需要注意,其持久化和读回UI的能力都需要依赖AppStorage。在阅读本文档前,建议提前阅读:[AppStorage](./arkts-appstorage.md),[PersistentStorage API文档](../../reference/apis-arkui/arkui-ts/ts-state-management.md#persistentstorage)。 11 12## 概述 13 14PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。 15 16PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。 17 18PersistentStorage的存储路径为module级别,即哪个module调用了PersistentStorage,数据副本存入对应module的持久化文件中。如果多个module使用相同的key,则数据为最先使用PersistentStorage的module,并且数据也会存入最先使用PersistentStorage的module里。 19 20PersistentStorage的存储路径在应用第一个ability启动时就已确定,为该ability所属的module。如果一个ability调用了PersistentStorage,并且该ability能被不同module的拉起, 那么ability存在多少种启动方式,就会有多少份数据副本。 21 22PersistentStorage功能上耦合了AppStorage,并且数据在不同module中使用也会有问题,因此推荐开发者使用PersistenceV2的globalConnect接口替换掉PersistentStorage的persistProp接口。PersistentStorage向PersistenceV2迁移的方案见[PersistentStorage->PersistenceV2](arkts-v1-v2-migration.md#persistentstorage-persistencev2)。PersistenceV2相关介绍参考文档[PersistenceV2](arkts-new-persistencev2.md)。 23 24## 限制条件 25 26PersistentStorage允许的类型和值有: 27 28- `number, string, boolean, enum` 等简单类型。 29- 可以被`JSON.stringify()`和`JSON.parse()`重构的对象,但是对象中的成员方法不支持持久化。 30- API12及以上支持Map类型,可以观察到Map整体的赋值,同时可通过调用Map的接口`set`, `clear`, `delete` 更新Map的值。且更新的值被持久化存储。详见[装饰Map类型变量](#装饰map类型变量)。 31- API12及以上支持Set类型,可以观察到Set整体的赋值,同时可通过调用Set的接口`add`, `clear`, `delete` 更新Set的值。且更新的值被持久化存储。详见[装饰Set类型变量](#装饰set类型变量)。 32- API12及以上支持Date类型,可以观察到Date整体的赋值,同时可通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。且更新的值被持久化存储。详见[装饰Date类型变量](#装饰date类型变量)。 33- API12及以上支持`undefined` 和 `null`。 34- API12及以上[支持联合类型](#支持联合类型)。 35 36PersistentStorage不允许的类型和值有: 37 38- 嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。 39 40持久化数据是一个相对缓慢的操作,应用程序应避免以下情况: 41 42- 持久化大型数据集。 43 44- 持久化经常变化的变量。 45 46PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。 47 48PersistentStorage和UI实例相关联,持久化操作需要在UI实例初始化成功后(即[loadContent](../../reference/apis-arkui/js-apis-window.md#loadcontent9-2)传入的回调被调用时)才可以被调用,早于该时机调用会导致持久化失败。 49 50```ts 51// EntryAbility.ets 52onWindowStageCreate(windowStage: window.WindowStage): void { 53 windowStage.loadContent('pages/Index', (err) => { 54 if (err.code) { 55 return; 56 } 57 PersistentStorage.persistProp('aProp', 47); 58 }); 59} 60``` 61 62## 使用场景 63 64 65### 从AppStorage中访问PersistentStorage初始化的属性 66 671. 初始化PersistentStorage: 68 69 ```ts 70 PersistentStorage.persistProp('aProp', 47); 71 ``` 72 732. 在AppStorage获取对应属性: 74 75 ```ts 76 AppStorage.get<number>('aProp'); // returns 47 77 ``` 78 79 或在组件内部定义: 80 81 82 ```ts 83 @StorageLink('aProp') aProp: number = 48; 84 ``` 85 86完整代码如下: 87 88 89```ts 90PersistentStorage.persistProp('aProp', 47); 91 92@Entry 93@Component 94struct Index { 95 @State message: string = 'Hello World'; 96 @StorageLink('aProp') aProp: number = 48; 97 98 build() { 99 Row() { 100 Column() { 101 Text(this.message) 102 // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果 103 Text(`${this.aProp}`) 104 .onClick(() => { 105 this.aProp += 1; 106 }) 107 } 108 } 109 } 110} 111``` 112 113- 新应用安装后首次启动运行: 114 1. 调用persistProp初始化PersistentStorage,首先查询在PersistentStorage本地文件中是否存在“aProp”,查询结果为不存在,因为应用是第一次安装。 115 2. 接着查询属性“aProp”在AppStorage中是否存在,依旧不存在。 116 3. 在AppStorage中创建名为“aProp”的number类型属性,属性初始值是定义的默认值47。 117 4. PersistentStorage将属性“aProp”和值47写入磁盘,AppStorage中“aProp”对应的值和其后续的更改将被持久化。 118 5. 在Index组件中创建状态变量\@StorageLink('aProp') aProp,和AppStorage中“aProp”双向绑定,在创建的过程中会在AppStorage中查找,成功找到“aProp”,所以使用其在AppStorage找到的值47。 119 120 **图1** PersistProp初始化流程 121 122 123 124- 触发点击事件后: 125 1. 状态变量\@StorageLink('aProp') aProp改变,触发Text组件重新刷新。 126 2. \@StorageLink装饰的变量是和AppStorage中建立双向同步的,所以\@StorageLink('aProp') aProp的变化会被同步回AppStorage中。 127 3. AppStorage中“aProp”属性的改变会同步到所有绑定该“aProp”的单向或者双向变量,在本示例中没有其他的绑定“aProp”的变量。 128 4. 因为“aProp”对应的属性已经被持久化,所以在AppStorage中“aProp”的改变会触发PersistentStorage,将新的改变写入本地磁盘。 129 130- 后续启动应用: 131 1. 执行PersistentStorage.persistProp('aProp', 47),首先在PersistentStorage本地文件查询“aProp”属性,成功查询到。 132 2. 将在PersistentStorage查询到的值写入AppStorage中。 133 3. 在Index组件里,\@StorageLink绑定的“aProp”为PersistentStorage写入AppStorage中的值,即为上一次退出应用存入的值。 134 135 136### 在PersistentStorage之前访问AppStorage中的属性 137 138该示例为反例。在调用PersistentStorage.persistProp或者persistProps之前使用接口访问AppStorage中的属性是错误的,因为这样的调用顺序会丢失上一次应用程序运行中的属性值: 139 140 141```ts 142let aProp = AppStorage.setOrCreate('aProp', 47); 143PersistentStorage.persistProp('aProp', 48); 144``` 145 146应用在非首次运行时,先执行AppStorage.setOrCreate('aProp', 47):属性“aProp”在AppStorage中创建,其类型为number,其值设置为指定的默认值47。“aProp”是持久化的属性,所以会被写回PersistentStorage磁盘中,PersistentStorage存储的上次退出应用的值丢失。 147 148PersistentStorage.persistProp('aProp', 48):在PersistentStorage中查找到“aProp”,值为刚刚使用AppStorage接口写入的47。 149 150### 在PersistentStorage之后访问AppStorage中的属性 151 152开发者可以先判断是否需要覆盖上一次保存在PersistentStorage中的值,如果需要覆盖,再调用AppStorage的接口进行修改,如果不需要覆盖,则不调用AppStorage的接口。 153 154```ts 155PersistentStorage.persistProp('aProp', 48); 156if (AppStorage.get('aProp') > 50) { 157 // 如果PersistentStorage存储的值超过50,设置为47 158 AppStorage.setOrCreate('aProp',47); 159} 160``` 161 162示例代码在读取PersistentStorage储存的数据后判断“aProp”的值是否大于50,如果大于50的话使用AppStorage的接口设置为47。 163 164 165### 支持联合类型 166 167PersistentStorage支持联合类型和undefined和null,在下面的示例中,使用persistProp方法初始化"P"为undefined。通过@StorageLink("P")绑定变量p,类型为number | undefined | null,点击Button改变P的值,视图会随之刷新。且P的值被持久化存储。 168 169```ts 170PersistentStorage.persistProp("P", undefined); 171 172@Entry 173@Component 174struct TestCase6 { 175 @StorageLink("P") p: number | undefined | null = 10; 176 177 build() { 178 Row() { 179 Column() { 180 Text(this.p + "") 181 .fontSize(50) 182 .fontWeight(FontWeight.Bold) 183 Button("changeToNumber").onClick(() => { 184 this.p = 10; 185 }) 186 Button("changeTo undefined").onClick(() => { 187 this.p = undefined; 188 }) 189 Button("changeTo null").onClick(() => { 190 this.p = null; 191 }) 192 } 193 .width('100%') 194 } 195 .height('100%') 196 } 197} 198``` 199 200 201### 装饰Date类型变量 202 203在下面的示例中,@StorageLink装饰的persistedDate类型为Date,点击Button改变persistedDate的值,视图会随之刷新。且persistedDate的值被持久化存储。 204 205```ts 206PersistentStorage.persistProp("persistedDate", new Date()); 207 208@Entry 209@Component 210struct PersistedDate { 211 @StorageLink("persistedDate") persistedDate: Date = new Date(); 212 213 updateDate() { 214 this.persistedDate = new Date(); 215 } 216 217 build() { 218 List() { 219 ListItem() { 220 Column() { 221 Text(`Persisted Date is ${this.persistedDate.toString()}`) 222 .margin(20) 223 224 Text(`Persisted Date year is ${this.persistedDate.getFullYear()}`) 225 .margin(20) 226 227 Text(`Persisted Date hours is ${this.persistedDate.getHours()}`) 228 .margin(20) 229 230 Text(`Persisted Date minutes is ${this.persistedDate.getMinutes()}`) 231 .margin(20) 232 233 Text(`Persisted Date time is ${this.persistedDate.toLocaleTimeString()}`) 234 .margin(20) 235 236 Button() { 237 Text('Update Date') 238 .fontSize(25) 239 .fontWeight(FontWeight.Bold) 240 .fontColor(Color.White) 241 } 242 .type(ButtonType.Capsule) 243 .margin({ 244 top: 20 245 }) 246 .backgroundColor('#0D9FFB') 247 .width('60%') 248 .height('5%') 249 .onClick(() => { 250 this.updateDate(); 251 }) 252 253 }.width('100%') 254 } 255 } 256 } 257} 258``` 259 260### 装饰Map类型变量 261 262在下面的示例中,@StorageLink装饰的persistedMapString类型为Map\<number, string\>,点击Button改变persistedMapString的值,视图会随之刷新。且persistedMapString的值被持久化存储。 263 264```ts 265PersistentStorage.persistProp("persistedMapString", new Map<number, string>([])); 266 267@Entry 268@Component 269struct PersistedMap { 270 @StorageLink("persistedMapString") persistedMapString: Map<number, string> = new Map<number, string>([]); 271 272 persistMapString() { 273 this.persistedMapString = new Map<number, string>([[3, "one"], [6, "two"], [9, "three"]]); 274 } 275 276 build() { 277 List() { 278 ListItem() { 279 Column() { 280 Text(`Persisted Map String is `) 281 .margin(20) 282 ForEach(Array.from(this.persistedMapString.entries()), (item: [number, string]) => { 283 Text(`${item[0]} ${item[1]}`) 284 }) 285 286 Button() { 287 Text('Persist Map String') 288 .fontSize(25) 289 .fontWeight(FontWeight.Bold) 290 .fontColor(Color.White) 291 } 292 .type(ButtonType.Capsule) 293 .margin({ 294 top: 20 295 }) 296 .backgroundColor('#0D9FFB') 297 .width('60%') 298 .height('5%') 299 .onClick(() => { 300 this.persistMapString(); 301 }) 302 303 }.width('100%') 304 } 305 } 306 } 307} 308``` 309 310### 装饰Set类型变量 311 312在下面的示例中,@StorageLink装饰的persistedSet类型为Set\<number\>,点击Button改变persistedSet的值,视图会随之刷新。且persistedSet的值被持久化存储。 313 314```ts 315PersistentStorage.persistProp("persistedSet", new Set<number>([])); 316 317@Entry 318@Component 319struct PersistedSet { 320 @StorageLink("persistedSet") persistedSet: Set<number> = new Set<number>([]); 321 322 persistSet() { 323 this.persistedSet = new Set<number>([33, 1, 3]); 324 } 325 326 clearSet() { 327 this.persistedSet.clear(); 328 } 329 330 build() { 331 List() { 332 ListItem() { 333 Column() { 334 Text(`Persisted Set is `) 335 .margin(20) 336 ForEach(Array.from(this.persistedSet.entries()), (item: [number, string]) => { 337 Text(`${item[1]}`) 338 }) 339 340 Button() { 341 Text('Persist Set') 342 .fontSize(25) 343 .fontWeight(FontWeight.Bold) 344 .fontColor(Color.White) 345 } 346 .type(ButtonType.Capsule) 347 .margin({ 348 top: 20 349 }) 350 .backgroundColor('#0D9FFB') 351 .width('60%') 352 .height('5%') 353 .onClick(() => { 354 this.persistSet(); 355 }) 356 357 Button() { 358 Text('Persist Clear') 359 .fontSize(25) 360 .fontWeight(FontWeight.Bold) 361 .fontColor(Color.White) 362 } 363 .type(ButtonType.Capsule) 364 .margin({ 365 top: 20 366 }) 367 .backgroundColor('#0D9FFB') 368 .width('60%') 369 .height('5%') 370 .onClick(() => { 371 this.clearSet(); 372 }) 373 374 } 375 .width('100%') 376 } 377 } 378 } 379} 380```