AppStorage:应用全局的UI状态存储
developer.huawei.com/consumer/cn…
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储
@StorageProp概述
@StorageProp初始化规则图示
框架行为
- 当@StorageProp(key)装饰的数值改变被观察到时,修改不会被同步回AppStorage对应属性键值key的属性中。
- 当前@StorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他的绑定该key的数据不会同步改变。
- 当@StorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回AppStorage中,但是会引起所属的自定义组件的重新渲染。
- 当AppStorage中key对应的属性发生改变时,会同步给所有@StorageProp(key)装饰的数据,@StorageProp(key)本地的修改将被覆盖
@StorageLink概述
@StorageLink初始化规则图示
框架行为
- 当@StorageLink(key)装饰的数值改变被观察到时,修改将被同步回AppStorage对应属性键值key的属性中。
- AppStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@StorageLink和单向@StorageProp)都将同步修改。
- 当@StorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回AppStorage中,还会引起所属的自定义组件的重新渲染
转换前代码
/**
*
* AppStorageCmpt.ets
* Created by unravel on 2024/5/3
* @abstract
*/
import { StorageDemoClass } from './StateDemoPage'
// 将设备的语言code存入AppStorage,默认值为en
Environment.envProp('languageCode', 'en');
// 初始化PersistentStorage
PersistentStorage.persistProp('aPersistentProp', 47);
AppStorage.setOrCreate('appStorageSimpleValue', 47);
AppStorage.setOrCreate('appStorageObject', new StorageDemoClass())
@Component
export struct AppStorageCmpt {
// appstorage
@StorageProp('appStorageSimpleValueKey')
appStoragePropSimpleValueVar: string = 'appStoragePropSimpleValueValue'
@StorageLink('appStorageObjectKey')
appStorageLinkObjectVar: StorageDemoClass = new StorageDemoClass()
// environment
@StorageProp('languageCode') lang: string = 'en';
// persistent
@StorageProp('aPersistentProp') persistentProp: string = 'persistentProp';
aboutToAppear(): void {
const appStoragePropSimpleValue: SubscribedAbstractProperty<string> = AppStorage.prop('appStoragePropSimpleValue')
appStoragePropSimpleValue.set('appStoragePropSimpleValue_AboutToAppear')
const appStorageLinkObject: SubscribedAbstractProperty<StorageDemoClass> = AppStorage.link('appStorageObject')
appStorageLinkObject.get().name = 'appStorageObject_AboutToAppear'
}
build() {
Column() {
Text('appStoragePropSimpleValue: ' + this.appStoragePropSimpleValueVar)
Text('appStorageLinkObject: ' + this.appStorageLinkObjectVar.name)
Text('lang: ' + this.lang)
Text('persistentProp: ' + this.persistentProp)
}.onClick(() => {
this.appStoragePropSimpleValueVar = 'cmpLocalStorageSimpleValue1'
this.appStorageLinkObjectVar.name = 'cmpLocalStorageObject.name'
this.lang = 'zh'
this.persistentProp = 'persistentProp1'
})
}
}
转换后代码
if (!("finalizeConstruction" in ViewPU.prototype)) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface AppStorageCmpt_Params {
appStoragePropSimpleValueVar?: string;
appStorageLinkObjectVar?: StorageDemoClass;
lang?: string;
persistentProp?: string;
}
import { StorageDemoClass } from "@bundle:com.unravel.myapplication/entry/ets/pages/StateDemoPage";
// 将设备的语言code存入AppStorage,默认值为en
Environment.envProp('languageCode', 'en');
// 初始化PersistentStorage
PersistentStorage.persistProp('aPersistentProp', 47);
AppStorage.setOrCreate('appStorageSimpleValue', 47);
AppStorage.setOrCreate('appStorageObject', new StorageDemoClass());
export class AppStorageCmpt extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.__appStoragePropSimpleValueVar = this.createStorageProp('appStorageSimpleValueKey', 'appStoragePropSimpleValueValue', "appStoragePropSimpleValueVar");
this.__appStorageLinkObjectVar = this.createStorageLink('appStorageObjectKey', new StorageDemoClass()
// environment
, "appStorageLinkObjectVar");
this.__lang = this.createStorageProp('languageCode', 'en', "lang");
this.__persistentProp = this.createStorageProp('aPersistentProp', 'persistentProp', "persistentProp");
this.setInitiallyProvidedValue(params);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: AppStorageCmpt_Params) {
}
updateStateVars(params: AppStorageCmpt_Params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__appStoragePropSimpleValueVar.purgeDependencyOnElmtId(rmElmtId);
this.__appStorageLinkObjectVar.purgeDependencyOnElmtId(rmElmtId);
this.__lang.purgeDependencyOnElmtId(rmElmtId);
this.__persistentProp.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__appStoragePropSimpleValueVar.aboutToBeDeleted();
this.__appStorageLinkObjectVar.aboutToBeDeleted();
this.__lang.aboutToBeDeleted();
this.__persistentProp.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
// appstorage
private __appStoragePropSimpleValueVar: ObservedPropertyAbstractPU<string>;
get appStoragePropSimpleValueVar() {
return this.__appStoragePropSimpleValueVar.get();
}
set appStoragePropSimpleValueVar(newValue: string) {
this.__appStoragePropSimpleValueVar.set(newValue);
}
private __appStorageLinkObjectVar: ObservedPropertyAbstractPU<StorageDemoClass>;
get appStorageLinkObjectVar() {
return this.__appStorageLinkObjectVar.get();
}
set appStorageLinkObjectVar(newValue: StorageDemoClass) {
this.__appStorageLinkObjectVar.set(newValue);
}
// environment
private __lang: ObservedPropertyAbstractPU<string>;
get lang() {
return this.__lang.get();
}
set lang(newValue: string) {
this.__lang.set(newValue);
}
// persistent
private __persistentProp: ObservedPropertyAbstractPU<string>;
get persistentProp() {
return this.__persistentProp.get();
}
set persistentProp(newValue: string) {
this.__persistentProp.set(newValue);
}
aboutToAppear(): void {
const appStoragePropSimpleValue: SubscribedAbstractProperty<string> = AppStorage.prop('appStoragePropSimpleValue');
appStoragePropSimpleValue.set('appStoragePropSimpleValue_AboutToAppear');
const appStorageLinkObject: SubscribedAbstractProperty<StorageDemoClass> = AppStorage.link('appStorageObject');
appStorageLinkObject.get().name = 'appStorageObject_AboutToAppear';
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
Column.onClick(() => {
this.appStoragePropSimpleValueVar = 'cmpLocalStorageSimpleValue1';
this.appStorageLinkObjectVar.name = 'cmpLocalStorageObject.name';
this.lang = 'zh';
this.persistentProp = 'persistentProp1';
});
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('appStoragePropSimpleValue: ' + this.appStoragePropSimpleValueVar);
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('appStorageLinkObject: ' + this.appStorageLinkObjectVar.name);
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('lang: ' + this.lang);
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('persistentProp: ' + this.persistentProp);
}, Text);
Text.pop();
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}
传递
这里我们的写法是显示的将storage传进了AppStorageCmpt这个组件。AppStorageCmpt和LocalStorageCmpt的初始化一模一样。
AppStorage只是一个特殊的LocalStorage
@StorageProp 分析
转换前后
转换成TS之后,和@State的操作类似。
- 将原有属性重写为getter和setter
- 声明一个以双下划线开头的类型为ObservedPropertyAbstractPU的私有属性,并通过this.createStorageProp创建该属性值
ViewPU
public createStorageProp(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU
这个函数我们在鸿蒙ACE-ArkUI构建(二)、渲染控制和构建过程 提到过,这里再看下具体实现
这里和 鸿蒙ACE-V1状态分析LocalStorage 几乎一样。
@LocalStorageProp是通过createLocalStorageProp创建,createLocalStorageProp内部调用的this.localStorage_.__createSync
@StorageProp是通过createStorageProp创建,createStorageProp内部调用的AppStorage.__createSync
AppStorage
AppStorage是LocalStorage的一个子类。所以它的__createSync和 鸿蒙ACE-V1状态分析LocalStorage 中@LocalStorageProp的createSync非常非常类似
public static __createSync(storagePropName: string, defaultValue: T, factoryFunc: SynchedPropertyFactoryFunc): ObservedPropertyAbstract
最终调用的AppStorage.setOrCreate的__createSync。而AppStorage.setOrCreate是一个单例LocalStorage。
后面的所有行为都和LocalStorage一致
get、set
@StorageProp装饰的变量的setter和getter实际上是操作的ObservedPropertyAbstract。和@State的作用一样(关联UI、UI刷新等)
最终操作的还是ObservedPropertyAbstract这个状态实例的get和set。所以流程和鸿蒙ACE-V1状态分析LocalStorage 一模一样
@StorageLink分析
转换前后
转换成TS之后,和@State的操作类似。
- 将原有属性重写为getter和setter
- 声明一个以双下划线开头的类型为ObservedPropertyAbstractPU的私有属性,并通过this.createStorageLink创建该属性值
ViewPU
public createStorageLink(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU
createStorageLink内部调用AppStorage.__createSync创建一个SynchedPropertyTwoWayPU。整个过程和createLocalStorageLink一模一样 。
可以参考 鸿蒙ACE-V1状态分析LocalStorage 章节 createLocalStorageLink的过程
小结
- @StorageLink的创建过程和@StorageProp的创建过程非常类似,只是factoryFun不一样
- @StorageLink的流程和@LocalStorageLink的一模一样。除了AppStorage是一个单例的LocalStorageLink这件事
- @LocalStorageProp和@LocalStorageLink与@Prop和@Link几乎一样,只不多过了一个查找对应key状态变量的过程。而这个key存储的就是类是和@Prop或@Link底层包装类是一样的
总结
- AppStorage是LocalStorage的一个单例子类,它包装的所有静态方法最终会访问单例的同名方法。LocalStorage的所有用法他都适用
- 因为它是单例的并且在程序启动时就创建了,所以可以全局共享
PersistentStorage:持久化存储UI状态
developer.huawei.com/consumer/cn…
PersistentStorage将选定的AppStorage属性保留在设备磁盘上。
应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。
UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage
怎么持久化
我们知道PersistentStorage是通过persistProp指定需要持久化的key
PersistentStorage
public static persistProp(key: string, defaultValue: T): void
最终调用了实例的persisProp方法
private static getOrCreate(): PersistentStorage
PersistentStorage.getOrCreate也是一个单例
private persistProp(propName: string, defaultValue: T): void
persisProp方法内又调用persisProp1。这个persisProp1才是真正的实现
persistProp1方法内通过AppStorage.link建立双向同步机制,并且将获取到的link保存在了link_ 中
links_ 是一个Map
在PersistentStorage初始化的时候就已经创建了一个links_的Map,以属性名为key,以SubscribedAbstractProperty为value
storage_ 实际的持久化操作者
在判断的时候,我们看到使用了PersistentStorage.storage_.has(propName) 判断属性存不存在。
这个storage_看了一下,是框架内部设置的一个storage。我们目前先不用关心
writeToPersistentStorage 持久化到storage
上面调用完persistProp之后,接着调用了writeToPersistentStorage,将数据持久化到storage
存储的时候会根据类型转换成Object,然后调用了PersistentStorage.storage_.set存储起来
MapInfo.toObject
SetInfo.toObject
DateInfo.toObject
readFromPersistentStorage 从storage中读取
可以看到读取和writeToPersistentStorage是一个相反的操作。写入时是将其他类型转换成对象,读取时是将对象转换成其他类型
而且,你看到了没,判断类型的时候巧妙的使用了MapInfo、SetInfo、DateInfo的replacer属性。类似的这种用法在TS中有相似的叫法,叫可辨识联合
为什么AppStorage中相同key变化的时候能够持久化
我们知道@Link状态变量变化的时候会调用syncPeerHasChanged
PersistentStorage
public syncPeerHasChanged(eventSource: ObservedPropertyAbstractPU)
这个方法里调用了this.write将需要持久化的状态变量写入了磁盘
private write(): void
write方法内部,通过遍历link_将所有需要持久化的状态变量持久化到磁盘
为什么link变化能够通知到PersistentStorage
我们回去看一下persisProp1这个方法,可以看到AppStorage调用setAndLink的时候将this传入了。这个this就是PersistentStorage单例
AppStorage
我们从上面的persistProp1方法中看到,创建link的时候实际上是调用的AppStorage.setAndLink
public static setAndLink(key: string, defaultValue: T, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty
接着调用了AppStorage单例的setAndLink。
LocalStorage
public setAndLink(propName: string, defaultValue: T, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty
在setAndLink内部又调用了this.link,将linkUser继续透传
public link(propName: string, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty | undefined
这个方法内继续将linkUser进行透传,并创建了SynchedPropertyTwoWayPU。SynchedPropertyTwoWayPU是@Link状态变量的实现类
SynchedPropertyTwoWayPU
constructor(source: ObservedPropertyObjectAbstract, owningChildView: IPropertySubscriber, thisPropertyName: PropertyInfo)
这个类的constructor方法中将linkUser作为持有状态变量的owningChildView传入
ObservedPropertyAbstractPU
constructor(subscriber: IPropertySubscriber, viewName: PropertyInfo)
我们找到SynchedPropertyTwoWayPU的父类ObservedPropertyAbstractPU。
因为owningView_不是ViewPU。所以被存储到了subscriberRefs_
ObservedPropertyAbstractPU
protected notifyPropertyHasChangedPU()
我们继续看subscriberRefs_使用的地方。发现在notifyPropertyHasChangedPU里面有调用。
而 notifyPropertyHasChangedPU是在状态变量变化之后会被调用
之后会调用subscriber上的syncPeerHasChanged这个方法。syncPeerHasChanged就是我们上面看过的方法
小结
- PersistentStorage保存Appstorage setAndLink方法创建出来的link
- 这个link会将PersistentStorage保存在它的subscriberRefs_中
- 这个link存储的状态变量发生变化的时候会调用notifyPropertyHasChangedPU,进而会调用到PersistentStorage的syncPeerHasChanged方法
- syncPeerHasChanged内完成属性的持久化
谁来持久化(storage_)
PersistentStorage.configureBackend
我们看到,在Index加载的时候就初始化了一个new Storage
JSPersistent
Storage对应的底层C++类是JSPersistent。一步步跟进,我们发现最终调用了StorageProxy的set方法
Set方法最终又调用了 StorageProxy::GetInstance()->GetStorage()->SetString(key, value);
StorageImpl
继续跟踪SetString,发现ACE里面总共有两个,一个是MockStorage,一个是StorageImpl。MockStorage是用于做单元测试的,我们不需要看。我们看以下StorageImpl
void StorageImpl::SetString(const std::string& key, const std::string& value)
可以看到最终调用了GetPreference进行存储。我们看一下filterName_是啥
fileName_是文件路径
可以看到最终是persistent_storage的文件名。我们自己定义文件名的时候就需要避免同名文件
小结
- PersistentStorage在调用persistProp的时候,会先从AppStorage尝试获取同名属性的link对象,如果没有则会根据默认值创建一个link并存储起来
- PersistentStorage的持久化是通过内部storage_ 来实现的,storage_实际上是一个名称为persistent_storage的Preference文件。理论上也有1W个key的限制
- 必须先调用PersistentStorage.persitProp再调用AppStorage的SetOrCreate才能持久化。因为我们看到persistProp内部会调用AppStorage.link获取link对象。如果先调用了AppStorage的SetOrCreate,因为此时内存里还没有对应key的值,必然使用默认值来初始化。导致把原来PersistentStorage中相同key的值覆盖掉
Environment:设备环境查询
developer.huawei.com/consumer/cn…
Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型
Environment环境变量怎么存入AppStorage
我们知道Environment是通过envProp将设备运行的环境变量存入AppStorage中
Environment
public static envProp(key: string, value: S): boolean
最终调用了实例的envProp方法
private static getOrCreate(): Environment
Environment.getOrCreate() 是一个单例
private envProp(key: string, value: S): boolean
envProp方法内通过AppStorage.setAndProp建立单向同步机制,并且将获取到的prop保存在了props_ 中
props_ 是一个Map
在Environment初始化的时候就已经创建了一个props_的Map,以属性名为key,以ObservedPropertyAbstract为value
envBackend_ 是实际的数据来源
我们看到envBackend_是通过configureBackend方法设置的
public static configureBackend(envBackend: IEnvironmentBackend): void
Environment数据变化怎么更新到AppStorage
private constructor()
可以看到在Environment初始化的时候,绑定了onValueChanged的回调函数
onValueChanged(key: string, value: any): void
在onValueChanged回调里直接调用了AppStorage.set方法将数据塞入。此时数据变化就会触发prop的更新
小结
- Environment保存Appstorage setAndProp方法创建出来的prop,同时注册一个环境变量变化的回调
- 在onValueChanged中重新设置环境变量的值,从而驱动UI更新
环境变量数据从哪里来(envBackend_)
Environment.configureBackend
在引擎引擎初始化的时候就调用了Environment.configureBackend初始化数据源
JSEnvironment
EnvironmentSetting 对应的底层C++类是JSEnvironment。以GetColorMode为例,看下怎么调用的
void JSEnvironment::JSBind(BindingTarget globalObj)
void JSEnvironment::GetColorMode(const JSCallbackInfo& args)
通过SystemProperties获取颜色模式之后,调用了args.SetReturnValue将数据返回
小结
- Environment在调用envProp的时候,会先从AppStorage尝试获取同名属性的prop对象,如果没有则会根据默认值创建一个prop并存储起来
- Environment的持久化是通过内部的envBackend_来实现的,envBackend_实际上是一个EnvironmentSetting类对应C++类为JSEnvironment
- Environment设置了一个回调,每次对应key的数据变化时就会调用AppStorage的set方法,进而驱动数据和UI的刷新
- 必须先调用Environment.env再调用AppStorage的SetOrCreate才能同步环境变量。因为我们看到envProp内部会调用AppStorage.setAndProp获取prop对象。如果先调用了AppStorage的SetOrCreate,之后再调用Environment.envProp,envProp方法内判断已经存在对应key的prop。就不会获取对应的环境变量了并且触发一个警告