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