查看原文
其他

鸿蒙中是如何实现UI自动刷新的?

Pika 鸿洋
2024-12-13

本文作者


作者:Pika

链接:

https://juejin.cn/post/7380357384776073266

本文由作者授权发布。

1文章介绍


从前几篇文章中,我们了解到ArkUI中针对UI进行刷新处理流程,对于应用开发者来说,ArkUI把驱动UI刷新的一系列操作通过状态管理装饰器,比如@State@Prop等暴露给开发者,通过引用这些装饰器修改的变量,我们能够实现自动的UI刷新处理。

值得注意的是,本章会涉及到api 9以上的内容,比如api 11,这些内容虽然在华为官网还没有暴露给普通开发者,但是我们可以通过open harmony docs 中提取查看这些内容,比如@Track 装饰器。因此如果大家想要提前了解鸿蒙Next的api,即便公司没有和华为签约,我们也还是能够通过open harmony docs去查看更高版本的内容。

https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/quick-start/arkts-track.md


本篇我们讲进入状态管理相关内容的学习,通过针对状态管理的学习,我们能够了解到以下知识点:
  1. 了解常见的状态管理,比如@State装饰器 是如何进行状态刷新。
  2. 鸿蒙api 9 class全量刷新导致的问题以及ArkUI后续优化的实现原理,比如@Track 装饰器。
  3. 了解到属性刷新驱动UI刷新的过程。

2状态管理例子

我们拿一个最简单的例子介绍一下状态管理,我们定义一个StateCase的Component,其中有一个button,当点击button的时候就会改变当前的ui展示,这里面的ui数据通过showRow这边变量管理。

@Component
struct StateCase{
  @State showRow:TestTrack = new TestTrack()
  build(){
    Row(){
      if (this.showRow.param1){
        Row(){
          Text("i am row")
        }
      }else{
        Column(){
          Text("i am colomn")
        }
      }

      Button("点我").onClick(()=>{
        this.showRow.param1 = !this.showRow.param1
      })
    }
  }
}


class TestTrack{

  param1:boolean = true

}
@State 修饰的变量,最终会被编译为一个ObservedPropertyObjectPU类的实现。
class StateCase extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1) {
        super(parent, __localStorage, elmtId);
        this.__showRow = new ObservedPropertyObjectPU(new TestTrack(), this"showRow");
        this.setInitiallyProvidedValue(params);
    }
    setInitiallyProvidedValue(params) {
        if (params.showRow !== undefined) {
            this.showRow = params.showRow;
        }
    }
    __showRow 的get方法
    get () {
        return this.__showRow.get();
    }
    __showRow的set方法
    set showRow(newValue) {
        this.__showRow.set(newValue);
    }
同时针对showRow的访问,都会被替换为针对__showRow变量的访问,比如下面的set与get方法。比如当我们点击button的时候,实际上就是调用了this.__showRow.set(newValue); 进行新值的赋予。
this.observeComponentCreation((elmtId, isInitialRender) => {
    ....
    Button.onClick(() => {
        this.showRow.param1 = !this.showRow.param1;

    });
    ....
});
这里我们就要停下来思考一下,ArkTS是怎么在TS的基础上实现的响应式?

响应式,其实本质上都是通过回调的思想实现的,ArkTS中在内部把这些回调的细节统统隐藏了,因此开发者们可以在不用关心这些细节的基础上,就很容易的实现UI的刷新。下面我们就来看,为什么ArkTS要千辛万苦的把我们普通声明的状态变量变成一个ObservedPropertyObjectPU对象。

3ObservedPropertyObjectPU 如何驱动UI刷新

ObservedPropertyObjectPU 的实现在ArkUI engine的/state_mgmt/src/lib/common 中,其实它是继承了ObservedPropertyPU的一个实现,ObservedPropertyObjectPU里面其实所有的set get方法,都会调用ObservedPropertyPUset/get方法。对应着上文例子中的__showRow get/set方法。
// class definitions for backward compatibility
class ObservedPropertyObjectPU<T> extends ObservedPropertyPU<T> {

}
我们来简单看一下ObservedPropertyPU的内部实现。

ObservedPropertyPU set方法

当外部UI需要发生改变的时候,就会通过set方法进行复制,比如改变 this.showRow.param1
 Button("点我").onClick(()=>{
        this.showRow.param1 = !this.showRow.param1
      })
实际上调用的就是set方法this.showRowset方法,我们拿arkui 4.1分支代码查看。这里注意,engine 4.1其实就是api11的代码,大家能够在openharmony中查看最新的代码分支情况,这些都是未来鸿蒙next的代码。即使个人开发者现在只能用api9的内容,但是我们我们还是可以查看到未公开的api11代码细节。【api 11代码能够更方便我们了解api9的一些状态管理弊端以及后续的优化方向】
  ObservedPropertyPU 类中

   set方法
  public set(newValue: T): void {
    如果两者是同一个变量,用=== 判断,则直接return不进行刷新,本次是无效刷新
    if (this.wrappedValue_ === newValue) {
      stateMgmtConsole.debug(`ObservedPropertyObjectPU[${this.id__()}, '${this.info() || "unknown"}']: set with unchanged value - ignoring.`);
      return;
    }
    stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value about to changed.`);
    把旧的,也就是上一个值用oldValue变量记录,方便后续进行UI刷新的判断。
    const oldValue = this.wrappedValue_;

    setValueInternal方法中会把this.wrappedValue_ 更新为newValue
    if (this.setValueInternal(newValue)) {
      TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.wrappedValue_,
        这里触发了UI刷新,在鸿蒙api 9 的版本只会走notifyPropertyHasChangedPU里面的内容刷新,这里大家可以思考一下
        this.notifyPropertyHasChangedPU.bind(this),
        this.notifyTrackedObjectPropertyHasChanged.bind(this));
    }
  }

  状态复制管理
  private setValueInternal(newValue: T): boolean {
    stateMgmtProfiler.begin("ObservedPropertyPU.setValueInternal");
    if (newValue === this.wrappedValue_) {
      stateMgmtConsole.debug(`ObservedPropertyObjectPU[${this.id__()}, '${this.info() || "unknown"}'] newValue unchanged`);
      stateMgmtProfiler.end();
      return false;
    }

    if (!this.checkIsSupportedValue(newValue)) {
      stateMgmtProfiler.end();
      return false;
    }

    // 解除旧的绑定
    this.unsubscribeWrappedObject();
    if (!newValue || typeof newValue !== 'object') {
      // undefined, null, simple type: 
      // nothing to subscribe to in case of new value undefined || null || simple type 
      this.wrappedValue_ = newValue;
    } else if (newValue instanceof SubscribableAbstract) {
      stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an SubscribableAbstract, subscribing to it.`);
      this.wrappedValue_ = newValue;
      (this.wrappedValue_ as unknown as SubscribableAbstract).addOwningProperty(this);
    } else if (ObservedObject.IsObservedObject(newValue)) {
      stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an ObservedObject already`);
      ObservedObject.addOwningProperty(newValue, this);
      this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(newValue);
      this.wrappedValue_ = newValue;
    } else {
      stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an Object, needs to be wrapped in an ObservedObject.`);
      this.wrappedValue_ = ObservedObject.createNew(newValue, this);
      this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(this.wrappedValue_);
    }
    stateMgmtProfiler.end();
    return true;
  }
这里面实际上主要做了以下三件事:
  1. 进行内部状态值更新,并设置回调。
  2. 绑定回调方,比如当属性发生通知的时候,通过回调告诉回调方。
  3. 把UI设置为脏处理,应用于后面UI的刷新流程。
第一件事,进行内部状态值更新,这里其实很容易理解,就是把set后的数值记录下来,这里其实是通过wrappedValue_记录的,setValueInternal里面会把wrappedValue_更新为最后一次set的值。
第二件事,绑定回调方。这里先通过this.unsubscribeWrappedObject(); 把旧的值解除绑定。这里面判断了是SubscribableAbstract还是ObservedObject 进行单独的处理。
  private unsubscribeWrappedObject() {
    if (this.wrappedValue_) {
      if (this.wrappedValue_ instanceof SubscribableAbstract) {
        (this.wrappedValue_ as SubscribableAbstract).removeOwningProperty(this);
      } else {
        ObservedObject.removeOwningProperty(this.wrappedValue_, this);

        // make sure the ObservedObject no longer has a read callback function
        // assigned to it
        ObservedObject.unregisterPropertyReadCb(this.wrappedValue_);
      }
    }
  }
最后根据监听的类型不同分别调用不同的方法把ViewPU注册进行,后续当属性发生改变的时候,ViewPU就能够得知,这里面可以看到,ViewPU它实现了IPropertySubscriber接口。
abstract class ViewPU extends NativeViewPartialUpdate
  implements IViewPropertiesChangeSubscriber 

interface IViewPropertiesChangeSubscriber extends IPropertySubscriber 
{
  // ViewPU get informed when View variable has changed
  // informs the elmtIds that need update upon variable change
  viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void ;
}
最后,到了最关键的UI刷新流程了,setValueInternal返回true的时候,就会执行notifyObjectValueAssignment进行回调,最终分为两个分支:如果class 里面没有@Track装饰器修饰的变量,则通过notifyPropertyHasChangedPU方法进行刷新(所以依赖这个class的UI都会被刷新),如果有的话,则通过notifyTrackedObjectPropertyHasChanged进行刷新(只依赖@Track装饰器修饰的变量的UI才会刷新)。
  if (this.setValueInternal(newValue)) {
      TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.wrappedValue_,
        this.notifyPropertyHasChangedPU.bind(this),
        this.notifyTrackedObjectPropertyHasChanged.bind(this));
    }
这里面涉及到了一个ArkUI中对于渲染效率的问题,我们重点看一下这里!

ArkUI的状态管理优化之路

在api 9 时,开发者能够以来的状态刷新装饰器一般有限,比如@State@Link 这些装饰器。它们都有一个缺陷,就是当依赖是一个类对象时,往往会导致不必要的刷新,我们来看一下例子代码。
@Component
struct StateCase{
  @State showRow:TestTrack = new TestTrack()
  build(){
    Row(){
      if (this.showRow.param1){
        Row(){
          Text("i am row")
        }
      }else{
        Column(){
          Text("i am colomn")
        }
      }
      // 冗余渲染,因为param2没有改变但是也会随着button点击发生重建
      Text(this.showRow.param2?"我是Text":"").width(this.param2Text())
      Button("点我").onClick(()=>{
        this.showRow.param1 = !this.showRow.param1

      })
    }
  }

  发生渲染时
  param2Text(){
    console.log("发生了渲染")
    return 100
  }
}


class TestTrack{

  param1:boolean = true
param2:boolean = true

}
当我们多次点击了button之后,我们可以观察到“发生了渲染”log输出了多变,这是因为Text发生了重建时会进行函数的调用。
这种现象被称为冗余渲染,即是param2 没有改动也会因为param1的修改导致后续以来param2的组件发生了重新绘制。
这里我们再回到ObservedPropertyPU的刷新流程:
    TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.wrappedValue_,
        this.notifyPropertyHasChangedPU.bind(this),
        this.notifyTrackedObjectPropertyHasChanged.bind(this));
    }
当属性发生改变的时候,会通过notifyObjectValueAssignment方法进行分类,一类是没有@Track 装饰器修饰的函数处理,另一类是有@Track 装饰器修饰的处理。
  public static notifyObjectValueAssignment(obj1: Objectobj2Object,
    notifyPropertyChanged() => void// notify as assignment (none-optimised)
    notifyTrackedPropertyChange: (propName) => void): boolean {
    // 默认处理,依赖class的属性控件都调用notifyPropertyChanged
    if (!obj1 || !obj2 || (typeof obj1 !== 'object') || (typeof obj2 !== 'object') ||
      (obj1.constructor !== obj2.constructor) ||
      TrackedObject.isCompatibilityMode(obj1)) {
      stateMgmtConsole.debug(`TrackedObject.notifyObjectValueAssignment notifying change as assignment (non-optimised)`)
      notifyPropertyChanged();
      return false;
    }
    // 有@Track 装饰器 处理,通过属性变量查找到对应的属性,然后只刷新依赖属性的UI
    stateMgmtConsole.debug(`TrackedObject.notifyObjectValueAssignment notifying actually changed properties (optimised)`)
    const obj1Raw = ObservedObject.GetRawObject(obj1);
    const obj2Raw = ObservedObject.GetRawObject(obj2);
    let shouldFakePropPropertyBeNotified: boolean = false;
    Object.keys(obj2Raw)
      .forEach(propName => {
        // Collect only @Track'ed changed properties
        if (Reflect.has(obj1Raw, `${TrackedObject.___TRACKED_PREFIX}${propName}`) &&
          (Reflect.get(obj1Raw, propName) !== Reflect.get(obj2Raw, propName))) {
          stateMgmtConsole.debug(`   ... '@Track ${propName}' value changed - notifying`);
          notifyTrackedPropertyChange(propName);
          shouldFakePropPropertyBeNotified = true;
        } else {
          stateMgmtConsole.debug(`   ... '${propName}' value unchanged or not @Track'ed - not notifying`);
        }
      });

  }
从上面可以看到,默认情况下,我们isCompatibilityMode 都会返回true,从而直接走到刷新流程,即调用参数为notifyPropertyChanged的方法,即我们外部传入的notifyPropertyHasChangedPU方法。
 public static isCompatibilityMode(obj: Object): boolean {
    return !obj || (typeof obj !== "object") || !Reflect.has(obj, TrackedObject.___IS_TRACKED_OPTIMISED);
  }
notifyPropertyHasChangedPU 方法是一个全量刷新方法,所有依赖了class的ViewPU属性都会进行重新渲染,即调用viewPropertyHasChanged方法。
  protected notifyPropertyHasChangedPU() {
    stateMgmtProfiler.begin("ObservedPropertyAbstractPU.notifyPropertyHasChangedPU");
    stateMgmtConsole.debug(`${this.debugInfo()}: notifyPropertyHasChangedPU.`)
    if (this.owningView_) {
      if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) {
        // send viewPropertyHasChanged right away
        this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getAllPropertyDependencies());
      } else {
        // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending
        this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending;
      }
    }
    this.subscriberRefs_.forEach((subscriber) => {
      if (subscriber) {
        if ('syncPeerHasChanged' in subscriber) {
          (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerHasChanged(this);
        } else  {
          stateMgmtConsole.warn(`${this.debugInfo()}: notifyPropertyHasChangedPU: unknown subscriber ID 'subscribedId' error!`);
        }
      }
    });
    stateMgmtProfiler.end();
  }  

viewPropertyHasChanged 这里终于来到我们之前说过的UI渲染逻辑,它会在内部调用markNeedUpdate方法把当前UI节点设置为脏状态,同时如果有@Watch 装饰器修饰的方法,在这个时候也会被回调。@Watch 装饰器也是api9 以上新增的方法,用于监听某个属性刷新然后触发方法调用。

https://juejin.cn/post/7349722583158521882


  viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIdsSet<number>): void {
    stateMgmtProfiler.begin("ViewPU.viewPropertyHasChanged");
    stateMgmtTrace.scopedTrace(() => {
      if (this.isRenderInProgress) {
        stateMgmtConsole.applicationError(`${this.debugInfo__()}: State variable '${varName}' has changed during render! It's illegal to change @Component state while build (initial render or re-render) is on-going. Application error!`);
      }

      this.syncInstanceId();

      if (dependentElmtIds.size && !this.isFirstRender()) {
        if (!this.dirtDescendantElementIds_.size && !this.runReuse_) {
          // mark ComposedElement dirty when first elmtIds are added
          // do not need to do this every time
          进行标记,进入UI刷新的流程
          this.markNeedUpdate();
        }
        stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged property: elmtIds that need re-render due to state variable change: ${this.debugInfoElmtIds(Array.from(dependentElmtIds))} .`)
        for (const elmtId of dependentElmtIds) {
          if (this.hasRecycleManager()) {
            this.dirtDescendantElementIds_.add(this.recycleManager_.proxyNodeId(elmtId));
          } else {
            this.dirtDescendantElementIds_.add(elmtId);
          }
        }
        stateMgmtConsole.debug(`   ... updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`)
      } else {
        stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged: state variable change adds no elmtIds for re-render`);
        stateMgmtConsole.debug(`   ... unchanged full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`)
      }
      回调@Watch 装饰器修饰方法
      let cb = this.watchedProps.get(varName)
      if (cb) {
        stateMgmtConsole.debug(`   ... calling @Watch function`);
        cb.call(this, varName);
      }

      this.restoreInstanceId();
    }, "ViewPU.viewPropertyHasChanged"this.constructor.name, varName, dependentElmtIds.size);
    stateMgmtProfiler.end();
  }
这里也就解释了,为什么@State 修饰的class变量,会产生冗余渲染的原因,因为所有依赖的ViewPU都会被标记重建。
回到上文,如果isCompatibilityMode返回false,即Reflect.has(obj, TrackedObject.___IS_TRACKED_OPTIMISED)为true的情况下,证明当前对象有@Track 属性,因此做的事情也比较简单,就是找到@Track 装饰器修饰的属性,并刷新只依赖了属性的ViewPU(getTrackedObjectPropertyDependencies 方法获取)。
  protected notifyTrackedObjectPropertyHasChanged(changedPropertyName : string) : void {
    stateMgmtProfiler.begin("ObservedPropertyAbstract.notifyTrackedObjectPropertyHasChanged");
    stateMgmtConsole.debug(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged.`)
    if (this.owningView_) {
      if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) {
        // send viewPropertyHasChanged right away
        this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getTrackedObjectPropertyDependencies(changedPropertyName, "notifyTrackedObjectPropertyHasChanged"));
      } else {
        // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending
        this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending;
      }
    }
    this.subscriberRefs_.forEach((subscriber) => {
      if (subscriber) {
        if ('syncPeerTrackedPropertyHasChanged' in subscriber) {
          (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerTrackedPropertyHasChanged(this, changedPropertyName);
        } else  {
          stateMgmtConsole.warn(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged: unknown subscriber ID 'subscribedId' error!`);
        }
      }
    });
    stateMgmtProfiler.end();
  }
至此,我们完成了整个@State 装饰器内部的逻辑分析,以及在api11会存在的@Track @Watch装饰器的流程,这些新增的装饰器都是为了解决ArkTS中存在的局限而产生。合理运用不同的装饰器,才能把ArkUI的性能发挥得更好。

回到上面的例子,如果param2不需要被param1刷新,我们只需要使用@Track装饰器标记param1即可,因此后续变化只会追踪param1的变化。更多例子可以观看这里 @Track

https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/quick-start/arkts-track.md


class TestTrack{

  @Track param1:boolean = true
  param2:boolean = true

}

ObservedPropertyPU get方法

ObservedPropertyPUget方法比较简单,核心逻辑就是返回set方法中设置的最新值,即wrappedValue_

  public get(): T {
    stateMgmtProfiler.begin("ObservedPropertyPU.get");
    stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get`);
    this.recordPropertyDependentUpdate();
    if (this.shouldInstallTrackedObjectReadCb) {
      stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get@Track optimised mode. Will install read cb func if value is an object`);
      ObservedObject.registerPropertyReadCb(this.wrappedValue_, this.onOptimisedObjectPropertyRead.bind(this));
    } else {
      stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: compatibility mode. `);
    }
    stateMgmtProfiler.end();
    return this.wrappedValue_;
  }
在后续UI重建会回调ViewPU 在 initialRender方法时,调用observeComponentCreation 放入的更新函数,此时就能够拿到最新的变量了,本例子就是showRow变量。

 this.observeComponentCreation((elmtId, isInitialRender) => {
            ... 
            if (this.showRow) {
                this.ifElseBranchUpdateFunction(0() => {
                    this.observeComponentCreation((elmtId, isInitialRender) => {


4总结


通过学习ArkUI中的状态管理,我们应该对ArkTS中的状态装饰器有了更加深入的理解,正是有了这些装饰器背后的运行机制,才能让开发者构建出低成本且丰富多彩的响应式UI框架。状态管理驱动UI刷新是ArkUI中面向开发者最核心的一部分,希望本文对你有所帮助。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

Android从上帝视角来看PackageManagerService
深入探索 APKTool:Android 应用的反编译与重打包工具
Android自定义Lint的二三事儿



扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存