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