鸿蒙ACE-V1状态分析AppStorage、PersistentStorage、Environment

80 阅读11分钟

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初始化规则图示

框架行为

  1. 当@StorageLink(key)装饰的数值改变被观察到时,修改将被同步回AppStorage对应属性键值key的属性中。
  2. AppStorage中属性键值key对应的数据一旦改变,属性键值key绑定的所有的数据(包括双向@StorageLink和单向@StorageProp)都将同步修改。
  3. 当@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

image.png

@StorageProp 分析

转换前后

转换成TS之后,和@State的操作类似。

  1. 将原有属性重写为getter和setter
  2. 声明一个以双下划线开头的类型为ObservedPropertyAbstractPU的私有属性,并通过this.createStorageProp创建该属性值

image.png

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

image.png

AppStorage

AppStorage是LocalStorage的一个子类。所以它的__createSync和 鸿蒙ACE-V1状态分析LocalStorage 中@LocalStorageProp的createSync非常非常类似

image.png

public static __createSync(storagePropName: string, defaultValue: T, factoryFunc: SynchedPropertyFactoryFunc): ObservedPropertyAbstract

最终调用的AppStorage.setOrCreate的__createSync。而AppStorage.setOrCreate是一个单例LocalStorage

后面的所有行为都和LocalStorage一致

image.png

get、set

@StorageProp装饰的变量的setter和getter实际上是操作的ObservedPropertyAbstract。和@State的作用一样(关联UI、UI刷新等)

最终操作的还是ObservedPropertyAbstract这个状态实例的get和set。所以流程和鸿蒙ACE-V1状态分析LocalStorage 一模一样

image.png

@StorageLink分析

转换前后

转换成TS之后,和@State的操作类似。

  1. 将原有属性重写为getter和setter
  2. 声明一个以双下划线开头的类型为ObservedPropertyAbstractPU的私有属性,并通过this.createStorageLink创建该属性值

image.png

ViewPU

public createStorageLink(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU

createStorageLink内部调用AppStorage.__createSync创建一个SynchedPropertyTwoWayPU。整个过程和createLocalStorageLink一模一样 。

可以参考 鸿蒙ACE-V1状态分析LocalStorage 章节 createLocalStorageLink的过程

image.png

小结

  1. @StorageLink的创建过程和@StorageProp的创建过程非常类似,只是factoryFun不一样
  2. @StorageLink的流程和@LocalStorageLink的一模一样。除了AppStorage是一个单例的LocalStorageLink这件事
  3. @LocalStorageProp和@LocalStorageLink与@Prop和@Link几乎一样,只不多过了一个查找对应key状态变量的过程。而这个key存储的就是类是和@Prop或@Link底层包装类是一样的

总结

  1. AppStorage是LocalStorage的一个单例子类,它包装的所有静态方法最终会访问单例的同名方法。LocalStorage的所有用法他都适用
  2. 因为它是单例的并且在程序启动时就创建了,所以可以全局共享

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方法

image.png

private static getOrCreate(): PersistentStorage

PersistentStorage.getOrCreate也是一个单例

image.png

private persistProp(propName: string, defaultValue: T): void

persisProp方法内又调用persisProp1。这个persisProp1才是真正的实现

persistProp1方法内通过AppStorage.link建立双向同步机制,并且将获取到的link保存在了link_

image.png

links_ 是一个Map

在PersistentStorage初始化的时候就已经创建了一个links_的Map,以属性名为key,以SubscribedAbstractProperty为value

image.png

storage_ 实际的持久化操作者

在判断的时候,我们看到使用了PersistentStorage.storage_.has(propName) 判断属性存不存在。

这个storage_看了一下,是框架内部设置的一个storage。我们目前先不用关心

image.png

writeToPersistentStorage 持久化到storage

上面调用完persistProp之后,接着调用了writeToPersistentStorage,将数据持久化到storage

存储的时候会根据类型转换成Object,然后调用了PersistentStorage.storage_.set存储起来

image.png

MapInfo.toObject

image.png

SetInfo.toObject

image.png

DateInfo.toObject

image.png

readFromPersistentStorage 从storage中读取

可以看到读取和writeToPersistentStorage是一个相反的操作。写入时是将其他类型转换成对象,读取时是将对象转换成其他类型

而且,你看到了没,判断类型的时候巧妙的使用了MapInfo、SetInfo、DateInfo的replacer属性。类似的这种用法在TS中有相似的叫法,叫可辨识联合

image.png

为什么AppStorage中相同key变化的时候能够持久化

我们知道@Link状态变量变化的时候会调用syncPeerHasChanged

PersistentStorage

public syncPeerHasChanged(eventSource: ObservedPropertyAbstractPU)

这个方法里调用了this.write将需要持久化的状态变量写入了磁盘

image.png

private write(): void

write方法内部,通过遍历link_将所有需要持久化的状态变量持久化到磁盘

image.png

为什么link变化能够通知到PersistentStorage

我们回去看一下persisProp1这个方法,可以看到AppStorage调用setAndLink的时候将this传入了。这个this就是PersistentStorage单例

image.png

AppStorage

我们从上面的persistProp1方法中看到,创建link的时候实际上是调用的AppStorage.setAndLink

public static setAndLink(key: string, defaultValue: T, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty

接着调用了AppStorage单例的setAndLink。

image.png

LocalStorage

public setAndLink(propName: string, defaultValue: T, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty

在setAndLink内部又调用了this.link,将linkUser继续透传

image.png

public link(propName: string, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty | undefined

这个方法内继续将linkUser进行透传,并创建了SynchedPropertyTwoWayPU。SynchedPropertyTwoWayPU是@Link状态变量的实现类

image.png

SynchedPropertyTwoWayPU

constructor(source: ObservedPropertyObjectAbstract, owningChildView: IPropertySubscriber, thisPropertyName: PropertyInfo)

这个类的constructor方法中将linkUser作为持有状态变量的owningChildView传入

image.png

ObservedPropertyAbstractPU

constructor(subscriber: IPropertySubscriber, viewName: PropertyInfo)

我们找到SynchedPropertyTwoWayPU的父类ObservedPropertyAbstractPU。

因为owningView_不是ViewPU。所以被存储到了subscriberRefs_

image.png

ObservedPropertyAbstractPU

protected notifyPropertyHasChangedPU()

我们继续看subscriberRefs_使用的地方。发现在notifyPropertyHasChangedPU里面有调用。

而 notifyPropertyHasChangedPU是在状态变量变化之后会被调用

image.png

之后会调用subscriber上的syncPeerHasChanged这个方法。syncPeerHasChanged就是我们上面看过的方法

小结

  1. PersistentStorage保存Appstorage setAndLink方法创建出来的link
  2. 这个link会将PersistentStorage保存在它的subscriberRefs_中
  3. 这个link存储的状态变量发生变化的时候会调用notifyPropertyHasChangedPU,进而会调用到PersistentStorage的syncPeerHasChanged方法
  4. syncPeerHasChanged内完成属性的持久化

谁来持久化(storage_)

PersistentStorage.configureBackend

我们看到,在Index加载的时候就初始化了一个new Storage

image.png

JSPersistent

Storage对应的底层C++类是JSPersistent。一步步跟进,我们发现最终调用了StorageProxy的set方法

Set方法最终又调用了 StorageProxy::GetInstance()->GetStorage()->SetString(key, value);

image.png

StorageImpl

继续跟踪SetString,发现ACE里面总共有两个,一个是MockStorage,一个是StorageImpl。MockStorage是用于做单元测试的,我们不需要看。我们看以下StorageImpl

image.png

void StorageImpl::SetString(const std::string& key, const std::string& value)

可以看到最终调用了GetPreference进行存储。我们看一下filterName_是啥

image.png

fileName_是文件路径

可以看到最终是persistent_storage的文件名。我们自己定义文件名的时候就需要避免同名文件

image.png

小结

  1. PersistentStorage在调用persistProp的时候,会先从AppStorage尝试获取同名属性的link对象,如果没有则会根据默认值创建一个link并存储起来
  2. PersistentStorage的持久化是通过内部storage_ 来实现的,storage_实际上是一个名称为persistent_storage的Preference文件。理论上也有1W个key的限制
  3. 必须先调用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方法

image.png

private static getOrCreate(): Environment

Environment.getOrCreate() 是一个单例

image.png

private envProp(key: string, value: S): boolean

envProp方法内通过AppStorage.setAndProp建立单向同步机制,并且将获取到的prop保存在了props_

image.png

props_ 是一个Map

在Environment初始化的时候就已经创建了一个props_的Map,以属性名为key,以ObservedPropertyAbstract为value

image.png

envBackend_ 是实际的数据来源

我们看到envBackend_是通过configureBackend方法设置的

public static configureBackend(envBackend: IEnvironmentBackend): void

image.png

Environment数据变化怎么更新到AppStorage

private constructor()

可以看到在Environment初始化的时候,绑定了onValueChanged的回调函数

image.png

onValueChanged(key: string, value: any): void

在onValueChanged回调里直接调用了AppStorage.set方法将数据塞入。此时数据变化就会触发prop的更新

image.png

小结

  1. Environment保存Appstorage setAndProp方法创建出来的prop,同时注册一个环境变量变化的回调
  2. 在onValueChanged中重新设置环境变量的值,从而驱动UI更新

环境变量数据从哪里来(envBackend_)

Environment.configureBackend

在引擎引擎初始化的时候就调用了Environment.configureBackend初始化数据源

image.png

JSEnvironment

EnvironmentSetting 对应的底层C++类是JSEnvironment。以GetColorMode为例,看下怎么调用的

void JSEnvironment::JSBind(BindingTarget globalObj)

image.png

void JSEnvironment::GetColorMode(const JSCallbackInfo& args)

通过SystemProperties获取颜色模式之后,调用了args.SetReturnValue将数据返回

image.png

小结

  1. Environment在调用envProp的时候,会先从AppStorage尝试获取同名属性的prop对象,如果没有则会根据默认值创建一个prop并存储起来
  2. Environment的持久化是通过内部的envBackend_来实现的,envBackend_实际上是一个EnvironmentSetting类对应C++类为JSEnvironment
  3. Environment设置了一个回调,每次对应key的数据变化时就会调用AppStorage的set方法,进而驱动数据和UI的刷新
  4. 必须先调用Environment.env再调用AppStorage的SetOrCreate才能同步环境变量。因为我们看到envProp内部会调用AppStorage.setAndProp获取prop对象。如果先调用了AppStorage的SetOrCreate,之后再调用Environment.envProp,envProp方法内判断已经存在对应key的prop。就不会获取对应的环境变量了并且触发一个警告