为什么 HarmonyOS 需要状态管理(源码级分析)

很多开发者第一次接触 ArkUI 都会有一个疑问:

为什么我改了一个变量,页面就自动刷新了?

例如:


@Entry
@Component
struct Index {

  @State count: number = 0

  build() {
    Column() {
      Text(`${this.count}`)

      Button("++")
        .onClick(() => {
          this.count++
        })
    }
  }
}

整个过程中,我们没有调用:


notifyDataSetChanged()

invalidate()

setState()

refresh()

update()

甚至没有一句刷新代码。

但是 UI 就刷新了。

很多人会觉得:

@State 帮我们刷新了页面。

其实这是一个非常大的误区。

真正刷新页面的不是 @State。

而是下面这一整套运行时。


ArkTS
    │
    ▼
Setter()
    │
    ▼
StateProxy
    │
    ▼
ObservedProperty
    │
    ▼
SubscriberManager
    │
    ▼
DirtyElement
    │
    ▼
PipelineContext
    │
    ▼
Build()
    │
    ▼
RenderNode Diff
    │
    ▼
RenderService
    │
    ▼
屏幕

真正的刷新动作,从来不是 @State。

而是 Runtime。

整个 ArkUI Runtime 才是真正的响应式引擎。


如果没有状态管理,会发生什么?

先回到 Android View 时代。

例如:


TextView tv;

int count = 0;

btn.setOnClickListener(v -> {
    count++;

    tv.setText(String.valueOf(count));
});

修改变量:


count++

UI 不知道。

必须:


tv.setText(...)

如果页面有几十个组件:


ImageView

TextView

Button

ListView

RecyclerView

Loading

Dialog

全部都需要开发者自己维护。

于是产生几个问题。

例如:


变量改了

↓

哪个控件要刷新?

↓

开发者自己决定

越来越复杂。


例如:

订单页面:


订单金额

优惠券

积分

红包

配送费

支付方式

余额

按钮状态

任意一个变化。

都有可能影响:


支付按钮

优惠金额

总金额

配送费

如果全部自己刷新。

最终代码会变成:


updatePrice();

updateCoupon();

updateVip();

updateDelivery();

updateButton();

updateHeader();

updateFooter();

updateDialog();

维护成本越来越高。

所以声明式 UI 出现以后。

变成:


数据变化

↓

框架决定刷新谁

开发者不用再关心:


哪个组件刷新

什么时候刷新

刷新多少

Runtime 全部完成。


声明式 UI 的核心思想

声明式 UI 最经典的一句话:

UI = f(State)

例如:


State

↓

UI

而不是:


UI

↓

修改UI

举个例子。

以前:


tv.setText("100");

现在:


Text(this.price.toString())

Text 不保存数据。

数据来自:


price

UI 永远只是数据的映射。

例如:


price = 100

↓

Text

↓

100

修改:


price = 200

UI 自动变成:


200

UI 从来没有主动更新。

只是重新计算。

这就是:


UI = f(State)

为什么 Build() 可以重新执行?

很多人以为:


build()

只执行一次

实际上:


build()

可能执行几百次

例如:


build() {

    console.log("build")

    Column(){

        Text(this.count.toString())

    }

}

点击:


++

控制台:


build

build

build

build

build

为什么?

因为 Runtime 每次都会:


重新计算UI树

很多人害怕。

每次都 build?

是不是很慢?

不会。

真正执行的是:


Build()

↓

Virtual Component

↓

Diff

↓

Dirty Node

↓

Patch

↓

RenderNode

不是:


全部销毁

全部创建

Build() 到底返回什么?

很多教程认为:


build()

↓

画页面

其实完全不是。

Build 返回的是:


组件树

例如:


build() {

    Column(){

        Text("A")

        Button("B")

    }

}

真正 Runtime 看到的是:


Column
    │
    ├────Text
    │
    └────Button

也就是说:


Build()

↓

Component Tree

不是 View。


Runtime 内部更接近:


ComponentNode

例如:


FrameNode

里面保存:


id

children

layout

render

state

modifier

event

所以:


build()

↓

创建ComponentNode

↓

不是立即画出来

Build 为什么那么快?

因为 Build 并不创建真正 View。

例如:

Android:


new TextView()

new Button()

new ImageView()

代价非常高。

但是 ArkUI:


Create TextNode

Create ButtonNode

只是创建:


Node Descriptor

例如:


Text

↓

{
 type:Text,
 content:"Hello"
}

一个普通对象。

不是 Native View。

所以速度非常快。


Runtime 到底保存了什么?

第一次 Build:


build()

↓

Tree A

例如:


Column

├──Text

├──Button

└──Image

第二次:


build()

↓

Tree B

Runtime:


Tree A

VS

Tree B

Diff:


Button

没变化

×

Text

变化

√

Image

没变化

于是:


只刷新Text

不是整个页面。


为什么不用重新 Layout?

继续举例。

第一次:


Text

宽100

第二次:


还是100

Layout:


不用重新计算

如果:


100

↓

300

那么:


Layout Dirty

重新布局。

所以:

刷新又分很多层。

例如:


State Dirty

↓

Build Dirty

↓

Layout Dirty

↓

Render Dirty

每一级成本都不同。


Dirty Flag(脏标记)源码解析

HarmonyOS Runtime 中,最重要的数据结构之一就是 Dirty Flag(脏标记)。

几乎所有状态更新最终都会变成:


node->MarkDirty(...)

可以理解为:


组件需要重新计算

但并不是所有脏标记都一样。

源码设计中通常会细分为不同级别,例如:


enum DirtyType {
    NONE,
    UPDATE,
    MEASURE,
    LAYOUT,
    RENDER
};

含义大致如下:


UPDATE
    ↓
重新执行 build()

MEASURE
    ↓
重新测量宽高

LAYOUT
    ↓
重新布局位置

RENDER
    ↓
重新绘制内容

举个例子:

修改文本内容:


this.title = "HarmonyOS"

如果只是文字发生变化,运行时可能只需要:


RENDER Dirty

而如果文本长度变长,导致组件宽度发生变化:


RENDER
    ↓
MEASURE
    ↓
LAYOUT

整个更新链就会继续向上传播。

因此,ArkUI 的优化重点并不是“不刷新”,而是尽量把刷新限制在最低成本的层级。

@State 源码实现(真正的响应式入口)

几乎所有 HarmonyOS 初学者都会认为:


@State count: number = 0

只是一个装饰器。

实际上并不是。

真正运行的时候,它已经不是你写的那个变量了。

很多人不知道,ArkTS 编译器(ETS Compiler)会在编译阶段,对所有状态变量进行代码转换(Compile Transform)。

例如你写的是:


@State
count: number = 0

编译之后,大致会变成类似下面这种形式(伪代码):


private __count: ObservedPropertySimple<number>;

constructor() {
    this.__count = new ObservedPropertySimple(
        0,
        this,
        "count"
    );
}

真正的数据已经不再是:


count

而是:


__count

真正保存数据的是:


ObservedPropertySimple

所以:

@State 从来不是数据。

它只是告诉编译器:

请把这个变量包装成响应式对象。


Getter / Setter 是怎么来的?

继续看。

开发者代码:


this.count++

实际上不会直接操作变量。

编译器继续生成:


get count() {
    return this.__count.get();
}

set count(value) {
    this.__count.set(value);
}

所以:


this.count++

真正执行的是:


let value = this.__count.get();

value++;

this.__count.set(value);

这里第一次出现了整个状态管理最重要的两个函数:


get()

set()

几乎整个 ArkUI Runtime 都围绕这两个接口运行。


get() 到底干了什么?

很多人认为:


get()

只是返回数据。

实际上完全不是。

源码思想更接近:


get() {

    collectDependency();

    return value;

}

真正重要的是:


collectDependency()

为什么?

因为 Runtime 必须知道:

谁正在使用这个变量。

例如:


Text(this.count.toString())

Runtime 会记录:


Text

依赖

count

以后:


count++

它才能知道:

应该刷新:


Text

而不是:


Button

Image

List

所以:

读取变量

其实就是:


建立依赖

而不是:


读取数据

这一点非常关键。


Dependency 是怎么收集的?

假设:


build() {

    Text(this.count.toString())

}

Runtime 执行 Build 时:

当前组件会压入一个栈。

例如:


Build Stack

↓

CurrentComponent

然后:


this.count

调用:


get()

get() 会读取:


CurrentComponent

于是:


count

↓

Subscriber

↓

CurrentComponent

依赖建立成功。

整个过程类似:


Build()

↓

Push Component

↓

Read State

↓

State 保存 Subscriber

↓

Pop Component

这就是依赖收集。


为什么必须在 Build() 中读取?

来看一个例子。


@State
count = 0

aboutToAppear() {

    console.log(this.count)

}

这里:


console.log()

也调用了:


get()

但是:

不会建立依赖。

为什么?

因为:

没有:


CurrentComponent

Runtime 判断:

当前不是 Build。

所以:


不需要建立依赖

因此:

只有:


Build()

@Builder

@BuilderParam

ForEach

LazyForEach

这些上下文。

读取状态。

才会建立:


Subscriber

set() 才是真正的刷新入口

继续来看:


this.count++

真正执行:


set(value)

set() 的伪代码:


set(value) {

    if (oldValue == value) {

        return;

    }

    this.value = value;

    notifySubscribers();

}

真正刷新页面的是:


notifySubscribers()

不是:


set()

set() 只是入口。


notifySubscribers() 做了什么?

假设:


count

被三个组件使用:


Header

Content

Footer

SubscriberList:


count

↓

Header

Content

Footer

修改:


count++

Runtime:


遍历 SubscriberList

例如:


for(auto node : subscribers){

    node->MarkDirty();

}

于是:


Header Dirty

Content Dirty

Footer Dirty

下一帧统一刷新。

而不是:


改一次

刷新一次

为什么不会立即刷新?

来看:


this.count++;

this.count++;

this.count++;

this.count++;

如果每一次:

立即 Build。

那么:


Build

Build

Build

Build

四次。

性能非常差。

所以 Runtime 使用:


Dirty Queue

第一次:


count++

↓

Header Dirty

第二次:


已经 Dirty

↓

忽略

第三次:


还是 Dirty

↓

忽略

最终:


只 Build 一次

所以:

很多开发者看到:


this.count++;

this.count++;

this.count++;

页面:


只刷新一次

就是这个原因。


Dirty Queue(脏队列)

Runtime 内部通常维护:


DirtyQueue

例如:


DirtyQueue

↓

ComponentA

ComponentB

ComponentC

每次:


MarkDirty()

都会:


Push Queue

但是:

如果已经存在:


Queue

↓

ComponentA

再次:


MarkDirty(ComponentA)

不会重复添加。

这样:


一次 Frame

↓

一个组件

↓

最多刷新一次

Scheduler(调度器)开始工作

很多人不知道。

真正刷新页面。

并不是:


MarkDirty()

↓

Build

中间还有一个:


Scheduler

流程:


set()

↓

notify

↓

MarkDirty

↓

DirtyQueue

↓

Scheduler

↓

VSync

↓

Build

↓

Diff

↓

Render

所以:

修改状态。

不会立刻刷新。

而是等待:


下一帧

这也是:

为什么连续:


count++

性能依然很高。


为什么是 VSync?

HarmonyOS 与 Android、Flutter 一样,都采用垂直同步(VSync)驱动渲染。

假设屏幕是:


60Hz

那么:

每:


16.67ms

才有一次:


Frame

Runtime:


16ms

↓

统一刷新

而不是:


改一次

刷新一次

这就是:


Frame Scheduler

一个完整的刷新链路

现在重新回头看:


this.count++

真正发生的是:


this.count++

↓

Setter

↓

ObservedProperty.set()

↓

notifySubscribers()

↓

SubscriberManager

↓

MarkDirty()

↓

DirtyQueue

↓

PipelineContext

↓

Scheduler

↓

VSync

↓

Build()

↓

生成新的 ComponentTree

↓

Diff

↓

FrameNode 更新

↓

RenderNode 更新

↓

RenderService

↓

GPU

↓

屏幕显示

这条链路贯穿了整个 ArkUI Runtime。

其中,@State 只是第一环,真正完成响应式更新的是 ObservedProperty → SubscriberManager → DirtyQueue → Scheduler → PipelineContext → RenderService 这一整套运行时机制。


为什么基本类型和对象使用不同实现?

细心的开发者会发现:


@State
count: number = 0

和:


@State
user = {
    name: "Tom"
}

虽然都使用 @State,但底层却不是同一个类。

编译器会根据数据类型自动选择不同的响应式包装器:


number
string
boolean

对应:


ObservedPropertySimple<T>

而:


Object
Array
Map
Class

对应:


ObservedPropertyObject<T>

为什么要拆成两个实现?

因为对象需要处理:

  • 属性变化
  • 引用变化
  • 深层嵌套对象
  • 数组增删改
  • 循环引用
  • Proxy/Observed 拦截

其实现复杂度远高于基本类型。

ObservedPropertyObject 源码深度解析

上一节我们分析了:


@State
count = 0

底层实际上对应:


ObservedPropertySimple

但是对象完全不是这样。

例如:


@State
user = {
    name: "Tom",
    age: 18
}

这里真正对应的是:


ObservedPropertyObject<User>

很多人觉得:

它只是换了个名字。

实际上整个实现完全不同。


为什么对象不能直接比较?

先来看最简单的问题。

基本类型:


this.count = 10

Runtime 判断:


oldValue == newValue

即可。

例如:


5

↓

10

变化了。

刷新。

但是对象呢?

例如:


this.user.name = "Jerry"

user 本身没有变。

只是:


name

Tom

↓

Jerry

如果 Runtime 仍然比较:


oldUser == newUser

得到:


true

因为:

还是同一个对象。

所以:


不会刷新

这就是为什么:

对象不能使用:


==

判断。


引用没有变

例如:


let obj = {
    name: "Tom"
}

修改:


obj.name = "Jerry"

内存实际上还是:


0x1000

只是里面:


Tom

↓

Jerry

地址没变。

所以:


Reference Equal

如果 Runtime 不知道:

里面发生变化。

自然不会刷新。


最简单的解决方案

最早很多框架都是这样:


this.user = {
    ...this.user,
    name: "Jerry"
}

为什么?

因为:

引用变了。

例如:

旧对象:


0x1000

新对象:


0x3000

Runtime:


Reference Changed

↓

Refresh

React 很多年都是这样工作的。

但是 HarmonyOS 不希望开发者:

天天:


...

Object.assign()

copy()

于是:

需要一种:

对象属性变化

也能监听。

怎么办?


Proxy 登场

ES6 提供了:


Proxy

例如:


let obj = {
    name:"Tom"
}

let proxy = new Proxy(obj,{
    get(){},
    set(){}
})

以后:


proxy.name="Jerry"

真正执行:


set()

不是:


直接修改对象

Runtime:

终于知道:

属性发生变化。

所以:

HarmonyOS Runtime 内部思想非常类似:


Object

↓

Proxy

↓

ObservedObject

↓

ObservedPropertyObject

编译器到底生成什么?

例如:

开发者:


@State
user = new User()

编译以后。

更接近:


private __user;

constructor(){

    let observed =
        ObservedObject.create(
            new User()
        )

    this.__user =
      new ObservedPropertyObject(
            observed,
            this,
            "user"
      )

}

注意。

真正包装两层。

第一层:


ObservedObject

第二层:


ObservedPropertyObject

很多开发者一直把两者混为一谈。

实际上职责完全不同。


ObservedObject 负责什么?

它只有一个目标:

监听:


对象里面

每一个属性

例如:


user.name

user.age

user.address

user.phone

全部监听。

而:

ObservedPropertyObject

负责:


通知UI

两者配合。

完成整个对象响应式。


为什么对象还要再包一层?

例如:


user.name="Jerry"

执行流程:


Proxy

↓

ObservedObject

↓

ObservedPropertyObject

↓

SubscriberManager

↓

Dirty Queue

如果没有:

ObservedObject。

Runtime 根本不知道:


name

发生变化

Proxy 的 set()

来看伪代码。


set(target,key,value){

    target[key]=value;

    notifyObjectPropertyHasChanged(
        key
    )

}

真正重要的是:


notifyObjectPropertyHasChanged()

例如:


user.age=20

Runtime:

知道:


age

变化

继续:

通知:


ObservedPropertyObject

最后:

刷新组件。


为什么数组 push() 能刷新?

很多人好奇。


@State
list=[]

执行:


this.list.push(item)

为什么刷新?

push()

不是:


赋值

实际上:

数组也是对象。

例如:


[]

↓

Array Object

Runtime Proxy:

拦截:


push

pop

splice

shift

unshift

sort

reverse

例如:


push()

内部:

实际上:


修改length

修改索引

通知变化

所以:

最终还是:


Proxy

↓

notify

为什么 Map、Set 默认不能响应?

例如:


@State
map = new Map()

执行:


this.map.set("A",1)

很多版本:

不会刷新。

为什么?

因为:

Proxy

只能监听:


属性

但是:

Map:

真正修改的是:


内部哈希表

不是:


obj.key

所以:

Runtime:

不知道。

企业里面一般:

重新赋值:


this.map =
    new Map(this.map)

或者:

使用:


ObservedMap

HarmonyOS 新版本开始也逐渐增强了集合类型的响应能力,但理解其底层仍然很重要:并不是所有引用类型都天然具备与普通对象相同的监听机制。


SubscriberManager 源码解析

前面已经提到:

真正刷新 UI 的。

不是:


State

而是:


SubscriberManager

它可以理解成:

整个 ArkUI 的:


调度中心

每一个 State 都有订阅者

例如:


Text(this.count.toString())

Text(this.count.toString())

Button()

Runtime:

记录:


count

↓

Subscriber List

↓

Text1

↓

Text2

Button:

没有读取:


count

所以:

不会加入。


更接近:


class ObservedProperty{

    Set<Subscriber> subscribers;

}

例如:


count

↓

{

Header,

Content,

Footer

}

以后:


count++

↓

遍历整个Set

为什么使用 Set?

不用:


Array

原因很简单。

假设:

Build:

执行三次。

每一次:

都读取:


count

如果:

Array:


Header

Header

Header

Header

重复。

最后:

刷新四次。

所以:

Runtime:

必须:


去重

Set:

天然:


唯一

于是:


std::unordered_set

或者:


HashSet

就是最佳选择。


Build 每次都会重新收集依赖

很多人认为:

依赖:

建立一次。

永久存在。

其实不是。

例如:


if(this.show){

    Text(this.count)

}

第一次:


show=true

Runtime:

建立:


count

↓

Text

第二次:


show=false

Text:

消失。

那么:

Subscriber:

必须:


删除

否则:

以后:


count++

还会:

刷新:

不存在的组件。

所以:

每一次:

Build。

都会:


重新收集依赖

↓

重新生成Subscriber Graph

而不是:

一直累积。

这也是为什么 ArkUI 在组件销毁、条件渲染(if/else)、ForEach 数据变化等场景下,依赖关系始终能够保持准确。


为什么不会发生内存泄漏?

假设:


if (this.login) {
    UserPanel()
}

登录时:


UserPanel

建立:


count

↓

UserPanel

退出登录:


UserPanel

销毁。

如果:

Subscriber:

不删除。

那么:


count++

Runtime:

还会:


通知:

UserPanel

但是:

组件:

已经不存在。

这就是:


Dangling Reference

严重的话甚至会造成:


Crash

因此在组件生命周期结束时,ArkUI Runtime 会统一执行类似:


SubscriberManager::RemoveSubscriber(componentId);

把当前组件从所有依赖关系中移除。

因此:


aboutToDisappear()

↓

Remove Subscriber

↓

GC

↓

释放组件

整个生命周期就完整闭环了。

@Prop 源码深度解析(为什么它是单向数据流)

开发 HarmonyOS 一段时间后,你一定写过这样的代码。

父组件:


@Entry
@Component
struct Parent {

    @State count: number = 0

    build() {
        Column() {
            Child({
                count: this.count
            })

            Button("+")
                .onClick(() => {
                    this.count++
                })
        }
    }
}

子组件:


@Component
struct Child {

    @Prop count: number

    build() {
        Text(this.count.toString())
    }
}

运行以后。

点击:


+

子组件:

立即刷新。

很多人认为:


Parent

↓

count

↓

Child

其实真正的数据流远比这个复杂。


编译器真正生成了什么?

开发者写的是:


@Prop
count:number

编译以后。

真正生成的大致如下(伪代码):


private __count:
SynchedPropertySimpleOneWay<number>;

初始化:


constructor(params){

    this.__count =
      new SynchedPropertySimpleOneWay(
            params.count,
            this,
            "count"
      )

}

注意。

这里已经不是:


ObservedPropertySimple

而是:


SynchedPropertySimpleOneWay

名字已经说明了一切。


OneWay

单向同步。

不是响应式变量。


为什么不用 @State?

很多人认为:


父组件

↓

State

↓

子组件

↓

还是 State

其实不是。

父组件:

仍然:


ObservedProperty

子组件:

变成:


SynchedProperty

两者职责不同。


ObservedProperty

负责拥有数据

而:


SynchedProperty

负责同步数据

它自己:

并不拥有数据。


为什么 Child 不能修改 @Prop?

例如:


@Prop
count:number

子组件:


Button("修改")
.onClick(()=>{

    this.count++

})

很多开发者第一次运行。

发现:

直接报错。

为什么?

因为:


@Prop

Readonly

真正 Setter:

更接近:


set(value){

    throw Error(
      "@Prop is readonly"
    )

}

或者:


set(value){

    console.warn()

}

不同版本略有区别。

但是思想一样。

禁止:


Child

↓

修改

Parent

为什么必须禁止?

假设:

允许:


Parent

↓

ChildA

↓

修改 count

同时:

还有:


ChildB

也在使用:


count

于是:

修改来源:


Parent

ChildA

ChildB

三个地方。

以后:

Bug:

会越来越难查。

所以:

HarmonyOS 和 React、

Compose、

SwiftUI

一样。

坚持:


单向数据流

即:


Parent

↓

Child

永远:

只有:

一个方向。


SynchedProperty 到底保存什么?

很多人误以为:


SynchedProperty

里面

保存数据

其实:

更准确地说。

它保存的是:


数据来源

例如:


Parent.count

真正关系:


Child

↓

SynchedProperty

↓

Parent ObservedProperty

所以:

Parent:

修改:


count

以后。

Child:

同步更新。


notify 流程

假设:


Parent

count++

真正流程:


ObservedProperty

↓

notify()

↓

SynchedProperty

↓

Child Dirty

↓

Build

所以:

Prop:

其实:

也是:

Subscriber。

只是:

不是:

UI。

而是:

另一个:

Property。

整个依赖图会变成:


ObservedProperty(count)

        │

        ▼

SynchedProperty

        │

        ▼

Child Component

        │

        ▼

Text

注意。

这里:

Subscriber:

已经不仅仅是:


Component

还可以是:


Property

为什么 Parent Build 后 Child 自动更新?

来看:

父组件:


this.count++

Parent:

Build:

重新执行。

生成:


Child({

count:this.count

})

Runtime:

发现:


参数

变化

于是:

执行:


SynchedProperty.set()

伪代码:


set(newValue){

    if(old!=new){

        value=newValue;

        notify();

    }

}

于是:

Child:

Dirty。

重新:

Build。


为什么 Child 不一定重新 Build?

很多人认为:

Parent:

Build。

Child:

一定:

Build。

实际上:

不是。

例如:


Child({

count:this.count

})

第一次:


count=10

第二次:

还是:


10

Runtime:

比较:


old

10

new

10

没有变化。

直接:


return

Child:

不会:

Build。

所以:

HarmonyOS:

父组件刷新。

并不意味着:

所有子组件刷新。


为什么多个 @Prop 不会互相影响?

例如:


Child({

name,

age,

sex,

phone

})

Runtime:

内部:

更接近:


SynchedProperty

↓

name

age

sex

phone

每一个:

都是:

独立:

Observed。

例如:

修改:


age

不会:

通知:


phone

所以:

局部刷新。

粒度:

非常细。


@Link 源码解析

终于来到:

HarmonyOS

最容易让人迷惑的:


@Link

例如:

父组件:


@State
count=0

Child({

count:$count

})

注意。

这里:

不是:


count

而是:


$count

为什么?

因为:

传递:

不是:

值。

而是:


引用

编译以后发生什么?

开发者:


$count

实际上:

编译器:

生成:


createLink(
    this.__count
)

真正:

Child:

收到:


Link Wrapper

而不是:


number

LinkWrapper 长什么样?

思想上:

更接近:


class LinkWrapper{

    source;

    get(){

        return source.get()

    }

    set(value){

        source.set(value)

    }

}

是不是很熟悉?

因为:

它根本:

没有数据。

只是:

代理。


Child 修改为什么 Parent 会变?

例如:

子组件:


this.count++

真正执行:


LinkWrapper.set()

↓

Parent ObservedProperty.set()

↓

notify()

↓

Parent Dirty

↓

Child Dirty

↓

UI刷新

所以:

真正修改的人。

一直都是:


Parent

Child:

只是:

拿到了:

Setter。

这就是:

双向绑定。


为什么必须使用 $?

很多开发者:

忘记:


$count

写成:


count

结果:

报错:

原因:

普通:


count

代表:


Value

而:


$count

代表:


ObservedProperty Reference

两者:

完全不同。

编译器:

也是:

根据:


$

决定:

生成:


LinkWrapper

还是:

普通:


Value

@Link 为什么性能比 @Prop 更高?

很多人觉得:

双向绑定。

应该:

更慢。

其实:

很多时候:

反而:

更快。

例如:

Prop:


Parent

↓

Build

↓

Child 参数变化

↓

SynchedProperty

↓

Child Build

Link:


Child

↓

直接

Parent State

↓

notify

少了一层:

参数同步。

所以:

很多:

表单。

Slider。

Input。

Switch。

企业项目。

都会:

优先:

使用:


@Link

而不是:


@Prop

当然,这并不意味着 @Link 应该滥用。它适用于确实需要子组件修改父组件状态的场景,例如输入框、开关、滑块等。对于普通展示型组件,保持 @Prop 的单向数据流更容易维护,也更符合大型项目的架构设计。

@Provide / @Consume 源码深度解析

很多开发者第一次看到:


@Provide
user: User = new User()

都会觉得:

这不就是全局变量吗?

其实完全不是。

它更像:


React

Context

或者:


Flutter

InheritedWidget

或者:


Vue

provide/inject

但是 HarmonyOS 的实现更加偏 Runtime。

真正保存数据的地方。

并不是:


Page

而是:


Context Stack

整个组件树。

都会维护:

一个:


ProviderMap

为什么需要 @Provide?

假设:

页面:


Home

里面:

有:


Home

 ├────Header

 ├────Body

 │      │

 │      ├────List

 │      │      │

 │      │      └────Item

 │      │

 │      └────Detail

 │

 └────Footer

现在:

需要:


UserInfo

给:


Item

如果不用:

Provide。

只能:

一级一级:

传。

例如:


Home

↓

Header(user)

↓

Body(user)

↓

List(user)

↓

Item(user)

Body:

其实:

根本不用:


user

但是:

不得不:

继续:

传。

这种:

就叫:


Props Drilling

React。

Vue。

Flutter。

都有。

HarmonyOS:

同样。

所以:

Provide:

诞生。


使用以后

Home:


@Provide
user=new User()

Item:


@Consume
user:User

中间:

所有组件。

不用:

写:


user

Runtime:

自动:

寻找。


编译以后发生什么?

开发者:


@Provide
user

真正:

编译:

更接近:


private __user:
ProvidedProperty<User>;

初始化:


this.__user=
new ProvidedProperty(
    user,
    this,
    "user"
)

注意。

这里:

又不是:


ObservedProperty

而是:


ProvidedProperty

职责:

只有:

一个。

注册:

Provider。


Provider 注册流程

组件:

创建。

执行:


aboutToAppear()

期间。

Runtime:

调用:


registerProvider()

伪代码:


ProviderMap.put(

    "user",

    provider

)

于是:

当前:

Context:

变成:


Page Context

↓

"user"

↓

Provider

ProviderMap 长什么样?

更接近:


unordered_map
<
string,

Provider
>

例如:


{

"user":0x1000,

"theme":0x2000,

"token":0x3000

}

后面:

Consume:

就是:

查:

这里。


Consume 初始化

例如:


@Consume
user

真正:

初始化:


this.__user=

new ConsumeProperty(

    "user",

    this

)

注意。

这里:

没有:

真正:

数据。

只有:

一个:


Key

例如:


"user"

后面:

Build:

开始。

Runtime:

寻找:

Provider。


Context Lookup(上下文查找)

假设:

组件树:


Page

 │

 ├──A

 │

 ├────B

 │

 ├────────C

 │

 └────────────Item

Item:

读取:


@Consume user

Runtime:

开始:


Item

没有。

继续:


C

没有。

继续:


B

没有。

继续:


A

没有。

继续:


Page

找到:


Provider(user)

于是:

绑定。

整个算法:

其实:

就是:


向父节点

不断查找

直到:

Root。


为什么不是全局?

很多人误会:

Provide:

就是:


Singleton

不是。

例如:

PageA:


@Provide
user="Tom"

PageB:


@Provide
user="Jerry"

两页:

互不影响。

因为:

Provider:

保存:

组件树。

不是:

Application。

真正:

关系:


PageA

↓

ProviderMapA

PageB

↓

ProviderMapB

所以:

同名:

也没关系。


最近原则(Nearest Provider)

来看:


Page

↓

Provide(user=Tom)

↓

A

↓

Provide(user=Jerry)

↓

B

↓

Consume(user)

最终:

拿到:

谁?

很多人:

猜:

Tom。

其实:

不是。

Runtime:

查找:

最近。

所以:

得到:


Jerry

整个:

Lookup:

过程:


B

↓

A

√

停止

不会:

继续:

找:

Page。

这就是:


Nearest Provider

几乎:

所有:

DI。

框架。

都是:

如此。


为什么修改 Provider 全部刷新?

假设:

三个组件:


A

B

C

全部:

Consume:


user

Provider:

维护:


Subscriber

例如:


user

↓

{

A,

B,

C

}

修改:


user.name="Jack"

Runtime:

流程:


Provider

↓

notify()

↓

Subscriber

↓

A Dirty

↓

B Dirty

↓

C Dirty

是不是:

和:

State:

一样?

没错。

Provider:

本质:

也是:

ObservedProperty。

只是:

增加:

Context。


为什么 Consume 不能独立存在?

例如:

页面:

只有:


@Consume
user

没有:


Provide

Runtime:

查找:


Root

仍然:

没有。

于是:

抛:

异常。

例如:


Provider Not Found

或者:


Undefined Consume

所以:

Consume:

必须:

对应:

Provide。


Provide 为什么可以动态切换?

例如:

登录。


@Provide
user=Tom

退出:


this.user=Guest

注意。

不是:

重新:

注册。

而是:


Provider

↓

ObservedProperty

↓

Set()

↓

Notify

整个:

Provider:

对象。

没变。

只是:

里面:

Value。

变了。

所以:

Consume:

全部:

自动:

刷新。


Provider 生命周期

组件:

创建:


Create

注册:


Register Provider

Build。

使用。

组件:

销毁:


aboutToDisappear()

Runtime:

自动:


ProviderMap.erase(

"user"

)

否则:

会:

出现:


Dangling Provider

造成:

内存:

泄漏。


为什么 Provider 不适合大量业务数据?

很多企业项目喜欢这样写:


@Provide
orderList

@Provide
memberList

@Provide
gameList

@Provide
chatList

@Provide
noticeList

@Provide
config

几十个:

Provide。

其实:

性能:

并不好。

原因:

Lookup:

虽然:

很快。

但是:

Context:

越来越大。

例如:


ProviderMap

↓

100+

Key

每个:

组件:

Build:

都会:

进行:

查找。

企业:

一般:

Provider:

只放:


Theme

Language

User

Permission

Router

Navigator

这种:

全局:

上下文。

而:

大量:

业务:

状态。

仍然:

建议:

使用:


ViewModel

Store

Repository

StateObject

管理。


Runtime 如何保证 Context 隔离?

来看:

两个:

Navigation。


Navigation

├──PageA

└──PageB

PageA:


Provide

Theme=Dark

PageB:


Provide

Theme=Light

Runtime:

实际上:

维护:

两个:


Context Tree

例如:


Navigation

    │

 ┌──┴──┐

 │     │

CtxA  CtxB

PageA:

永远:

访问:


CtxA

不会:

跑到:


CtxB

因此:

Navigation。

Tabs。

Dialog。

Sheet。

Popover。

都能:

保持:

Context:

独立。


Provide 为什么不能跨应用?

因为:

Provider。

本质:

只是:

Runtime:

里面:

一个:


Context Map

生命周期:

跟随:

组件树。

不是:

Application。

更不是:

Ability。

因此:

如果:

需要:

跨:

Ability。

共享。

只能:

使用:


AppStorage

PersistentStorage

Preferences

Database

DataShare

而:

不是:

Provide。


@Provide / @Consume 完整运行流程

整个 Runtime 的执行链路可以总结为:


@Provide

↓

ProvidedProperty

↓

ProviderMap Register

↓

Context Tree

↓

@Consume

↓

Context Lookup

↓

Bind Provider

↓

Subscriber Register

↓

Provider.set()

↓

Notify Subscribers

↓

MarkDirty()

↓

DirtyQueue

↓

Scheduler

↓

Build()

↓

Diff

↓

FrameNode

↓

RenderNode

↓

RenderService

↓

GPU

↓

屏幕刷新

可以看到,@Provide / @Consume 并没有创造一套新的响应式机制,而是在 ObservedProperty 之上增加了一层 Context 查找 + Provider 注册。真正的刷新链路与 @State、@Prop、@Link 最终都会汇聚到同一个 Runtime 调度系统。

@Observed 与 @ObjectLink 源码级深度解析

如果说:


@State

是 HarmonyOS 最常用的状态。

那么:


@Observed

@ObjectLink

就是整个状态管理里面最难理解的部分。

几乎所有 HarmonyOS 面试。

都会问:

为什么修改对象属性,有时候页面刷新,有时候又不刷新?

真正原因。

就在这一章。


一个经典面试题

来看代码:


class User {

    name:string="Tom"

}

@Entry
@Component
struct Index{

    @State
    user:User=new User()

    build(){

        Column(){

            Text(this.user.name)

            Button("修改")
                .onClick(()=>{

                    this.user.name="Jerry"

                })

        }

    }

}

很多开发者觉得:

一定刷新。

但是不同版本、不同对象结构、不同场景下。

结果并不一致。

为什么?

因为:

真正监听的。

并不是:


User

而是:


ObservedObject

如果:

User。

没有成为:


ObservedObject

Runtime。

无法知道:


name

发生变化

@Observed 到底是什么?

很多教程都会说:

@Observed 可以监听对象。

其实这句话。

并不准确。

真正来说。

它做的是:


让一个 Class

拥有响应式能力

例如:

开发者:


@Observed
class User{

    name:string="Tom"

}

编译以后。

实际上:

更接近:


User

↓

Generate Metadata

↓

Enable Proxy

↓

ObservedObject.create()

真正变化的是:

Class。

不是变量。

这一点。

非常重要。


为什么必须修饰 Class?

来看:


@State

user=new User()

这里只能知道:


user

发生改变

例如:


this.user=new User()

可以。

但是:


this.user.name="Jack"

谁知道?

没人知道。

因为:

真正变化的是:


name

不是:


user

于是:

需要:

整个:

Class。

支持:

属性监听。

所以:

HarmonyOS:

选择:


@Observed

修饰 Class

而不是:

变量。


编译器到底生成了什么?

开发者:


@Observed
class User{

    name="Tom"

    age=18

}

编译以后。

更接近:


class User{

    __metadata__={

        observed:true

    }

}

Runtime:

发现:


observed=true

于是:

创建:


ObservedObject

不是:

普通:


Object

ObservedObject 到底是什么?

真正:

Runtime。

里面:

更接近:


class ObservedObject{

    proxy;

    subscribers;

    target;

}

三个东西。

最重要。


proxy

负责:

监听。


target

真正对象。


subscriber

负责:

通知。


创建流程

例如:


new User()

真正:

Runtime:

执行:


new User()

↓

ObservedObject.create()

↓

Proxy

↓

ObservedPropertyObject

↓

UI

所以。

真正:

UI。

拿到的。

其实:

不是:


User

而是:


Proxy<User>

Proxy 真正拦截什么?

例如:


user.name="Jack"

真正:

执行:


Proxy.set(

"name",

"Jack"

)

不是:

直接:

修改。

伪代码:


set(target,key,value){

    target[key]=value;

    notifyPropertyChanged(key);

}

真正重要的是:


notifyPropertyChanged

为什么知道是哪一个属性?

例如:

修改:


user.name="Jack"

Runtime:

收到:


key

↓

name

修改:


user.age=20

收到:


age

所以:

Runtime。

知道:

到底:

哪一个:

属性:

变化。

不是:

整个对象。


Property Dependency(属性级依赖)

很多人认为:

Subscriber。

保存:

对象。

实际上。

更细。

例如:


Text(user.name)

Text(user.age)

真正:

依赖:

不是:


user

而是:


user.name

↓

Text1

还有:


user.age

↓

Text2

于是:

修改:


user.name="Jerry"

Runtime:

真正:

通知:


Text1

不会:

刷新:


Text2

是不是:

比:

State。

粒度:

更细?

没错。

HarmonyOS:

对象。

可以:

做到:

属性级刷新。


Subscriber Graph

整个:

依赖图。

变成:


             User

        /      |      \

     name     age     phone

      │        │         │

      ▼        ▼         ▼

    Text1    Text2    Text3

修改:


phone

只会:

刷新:


Text3

这就是:

HarmonyOS。

性能。

非常高。

的重要原因。


为什么深层对象又失效?

来看:


class Address{

    city="北京"

}

@Observed
class User{

    address=new Address()

}

修改:


user.address.city="上海"

很多人发现。

又:

不刷新。

为什么?

因为:

真正:

Observed。

只有:


User

不是:


Address

Address。

还是:

普通对象。

所以:

Proxy。

不会:

继续:

往里面。

代理。


多层代理

真正:

Runtime。

需要:

变成:


User Proxy

        │

        ▼

Address Proxy

        │

        ▼

City

否则。

只能:

监听:

第一层。


所以 Address 也要 @Observed

例如:


@Observed
class Address{

    city="北京"

}

@Observed
class User{

    address=new Address()

}

现在。

整个:

对象树。

全部:

拥有:

Proxy。

修改:


user.address.city="上海"

终于。

可以:

通知。

Runtime。


Object Graph(对象图)

真正:

Runtime。

里面。

更接近:


User

 │

 ├────name

 │

 ├────age

 │

 └────Address

          │

          ├────city

          │

          └────street

所有:

节点。

都有:

Subscriber。

整个。

形成:

Object Graph。

不是:

Tree。

因为:

可能:

多个对象。

引用:

同一个:

Address。


为什么不能无限递归 Proxy?

来看:


class A{

    b:B

}

class B{

    a:A

}

形成:


A

↓

B

↓

A

如果:

无限:

Proxy。

最终:

Stack Overflow。

所以:

Runtime。

内部:

维护:

一个:


Visited Map

例如:


unordered_map

<Object*,

ObservedObject*>

已经:

代理。

直接:

返回。

不会:

再次:

创建。


@ObjectLink 真正解决什么?

终于来到:

整个:

HarmonyOS。

最容易。

混淆。

的:


@ObjectLink

很多人。

一直认为:

它。

就是:


@Link

对象版

其实。

不是。

它。

解决的是:

对象身份(Identity)保持的问题。


例如:

父组件:


@State
user = new User()

子组件:


Child({
    user: this.user
})

如果:

子组件:

只是:


@Prop
user: User

那么:

收到的是:

对象。

但是:

对象里面。

每一个:

属性。

依赖。

不会:

继续。

传递。


真正:

ObjectLink。

编译以后。

生成:


ObjectLinkProperty

而不是:


SynchedProperty

它:

保存:

不是:

值。

而是:


ObservedObject Reference

于是:

父组件:

和:

子组件。

真正:

共享:

同一个:

Proxy。

整个:

关系:


Parent

↓

ObservedObject

↓

ObjectLink

↓

Child

↓

Text(user.name)

以后:

父组件:

修改:


user.name="Jack"

子组件。

不用:

重新:

传参。

因为:

两边。

拿到。

就是:

同一个:

Proxy。


Object Identity(对象身份)

这是很多框架都强调的概念。

例如:


0x1000

表示:

一个:

User。

Parent:

持有:


0x1000

Child:

如果:

也是:


0x1000

那么:

修改:

任何:

属性。

双方:

立即:

可见。

这就是:

Identity Sharing。

而:

如果:

Child:

拿到的是:


Copy

例如:


0x5000

那么:

之后:

两边。

已经:

没有:

任何:

联系。


ObjectLink 为什么性能更高?

来看:

Prop:


Parent

↓

Build

↓

Parameter Compare

↓

Child Build

ObjectLink:


Property Changed

↓

ObservedObject

↓

Subscriber

↓

Child Dirty

整个:

过程。

不需要:

参数:

重新:

同步。

也:

不用:

重新:

创建:

对象。

因此。

企业:

大型:

列表。

聊天。

商品。

IM。

联系人。

几乎:

都会:

优先:

使用:


@ObjectLink

共享:

对象。

而不是:

不断:

复制:

对象。


本章小结:对象响应式完整链路

当执行:


user.address.city = "上海"

真正经历的 Runtime 流程如下:


Address Proxy.set()

↓

ObservedObject

↓

PropertyChanged(city)

↓

SubscriberManager

↓

Property Dependency Graph

↓

MarkDirty()

↓

DirtyQueue

↓

Scheduler

↓

Build()

↓

Diff

↓

FrameNode

↓

RenderNode

↓

GPU

↓

屏幕刷新

可以看到,@Observed 负责的是让对象具备响应式能力,而 @ObjectLink 负责的是在组件之间共享同一个响应式对象实例。两者配合后,HarmonyOS 才能实现真正的深层对象、细粒度、跨组件响应式更新。

@Watch 源码级深度解析(HarmonyOS 最容易被误解的装饰器)

如果说:


@State

负责:

数据。

那么:


@Watch

负责:

副作用(Side Effect)。

很多开发者一直认为:

@Watch 用来刷新 UI。

这是错误的。

真正刷新 UI 的。

永远只有:


ObservedProperty

↓

SubscriberManager

↓

DirtyQueue

↓

Build

而:


@Watch

只是:

监听。

它不会参与:

任何:

UI。

刷新。


一个经典例子

例如:


@Entry
@Component
struct Index{

    @State
    @Watch("countChanged")
    count:number=0

    countChanged(){

        console.log("变化")

    }

}

执行:


this.count++

控制台:


变化

很多人认为:


Watch

↓

刷新页面

其实。

真正:

顺序。

不是。


真正执行顺序

真正:

Runtime。

流程。

更接近:


Setter

↓

ObservedProperty.set()

↓

Value Changed

↓

Watch Callback

↓

notifySubscribers()

↓

MarkDirty()

↓

Scheduler

↓

Build()

注意。

Watch。

发生:

比:

Build。

更早。

这一点。

很多人。

不知道。


为什么先执行 Watch?

来看:


@Watch("onPriceChanged")
@State
price=100

onPriceChanged(){

    console.log(this.price)

}

修改:


price=200

如果:

Build。

先执行。

那么:

Watch。

里面。

读取:


price

已经:

不是:

最新:

状态。

因此。

必须:

保证:


Value

↓

Watch

↓

UI

编译器到底生成什么?

开发者:


@Watch("priceChanged")

真正:

编译。

更接近:


ObservedPropertySimple(

100,

this,

"price",

this.priceChanged

)

可以看到。

Watch。

其实:

就是:

注册:

一个:


Callback

不是:

新的:

Property。


ObservedProperty 内部结构

真正:

源码。

思想。

更接近:


class ObservedProperty{

    value;

    subscribers;

    watchers;

}

里面。

其实:

有:

两份:

列表。


Subscriber

负责:

刷新。

还有:


Watcher

负责:

回调。

两者。

互不影响。


set() 真正源码流程

很多人。

一直认为。

Setter。

只有:

几十行。

实际上。

真正:

流程。

更复杂。

例如:


set(value){

    if(old==value){

        return;

    }

    old=value;

    notifyWatchers();

    notifySubscribers();

}

注意。

真正。

先:

Watch。

后:

Subscriber。


notifyWatchers()

例如:

三个:

Watch。


@Watch("A")

@Watch("B")

@Watch("C")

真正:

Runtime。

保存:


watchers

↓

A

↓

B

↓

C

修改:


price

执行:


for(

watcher

:

watchers

){

watcher();

}

然后。

才:

刷新。

UI。


多个 Watch 顺序

很多人。

问:

到底:

哪个:

先执行?

答案:

就是:

注册:

顺序。

例如:


@Watch("A")

@Watch("B")

@Watch("C")

真正:


A

↓

B

↓

C

不是:

随机。


Watch 为什么不能监听普通变量?

例如:


count=0

不是:


@State

即使:

写:


@Watch("changed")

也:

不会:

触发。

为什么?

因为:

Watch。

绑定。

的是:


ObservedProperty

普通:

变量。

没有:

Setter。

Runtime。

根本:

不知道:

什么时候:

变化。


Watch 为什么不能监听对象属性?

来看:


@State

user=new User()

@Watch("changed")

user

修改:


user.name="Jack"

很多人。

发现。

Watch。

没有:

执行。

为什么?

因为:

Watch。

监听:

的是:


user

不是:


user.name

引用。

没有:

变化。

所以:

不会:

通知。

只有:


user=new User()

才:

触发。


如何监听对象属性?

真正。

企业。

写法。

一般:

配合:


@Observed

例如:


@Observed
class User{

    @Trace

    name="Tom"

}

或者:

使用:


ObservedV2

后面。

会讲。

V2。

状态管理。


Watch 会不会死循环?

这是:

面试。

最经典。

的问题。

例如:


@Watch("changed")

@State

count=0

changed(){

    this.count++

}

运行:


count++

发生:


count++

↓

Watch

↓

count++

↓

Watch

↓

count++

↓

Watch

无限:

递归。

最终:


Stack Overflow

是不是?

Runtime。

很傻?

当然。

不是。


Runtime 如何避免递归?

真正:

内部。

维护:

一个:


Updating Flag

例如:


bool updating=false;

执行:


set(){

if(updating){

return;

}

updating=true;

notifyWatch();

updating=false;

}

于是:

Watch。

里面。

再次:

修改。

同一个:

Property。

直接:

忽略。

避免:

无限。

递归。


Watch 可以修改其他 State

来看:


@State

price=100

@State

vipPrice=0

@Watch("changed")

price

changed(){

    vipPrice=

    price*0.8

}

真正:

流程。


price

↓

Watch

↓

vipPrice

↓

vipPrice Dirty

↓

UI

完全。

没有。

问题。

因为:

不是:

同一个:

Property。


Watch 能发网络请求吗?

当然。

可以。

例如:


@Watch("keywordChanged")

@State

keyword=""

keywordChanged(){

    search()

}

但是。

企业。

一般。

都会:

加:


Debounce

否则。

输入:


HarmonyOS

十个:

字符。

发送:

十次:

请求。

服务器:

直接:

爆炸。

真正:

企业。

写法:


Input

↓

Watch

↓

300ms Debounce

↓

Request

↓

Result

↓

State

↓

UI

Watch 为什么不能做耗时操作?

例如:


changed(){

sleep(5s)

}

Runtime:

真正:

执行:


Setter

↓

Watch

↓

Build

Watch。

阻塞。

意味着:

Build。

不能:

开始。

最终:

页面。

卡死。

所以。

Watch。

应该:

只做:


数据转换

状态同步

事件通知

耗时:

操作。

应该:

异步。

例如:


changed(){

taskPool.execute(()=>{

request()

})

}

Watch 与生命周期谁先执行?

来看:

页面:

初始化。


Create

↓

aboutToAppear()

↓

Build()

如果:

初始化:

修改:

State。

例如:


aboutToAppear(){

count=10

}

真正:

执行:


Setter

↓

Watch

↓

Build

不是:


Build

↓

Watch

企业为什么很少使用 Watch?

很多人。

发现。

大厂。

代码。

几乎。

没有:


@Watch

为什么?

因为:

Watch。

容易:

造成:

状态:

分散。

例如:


price

↓

Watch

↓

vipPrice

↓

Watch

↓

discount

↓

Watch

↓

button

整个:

状态。

形成:

链式。

依赖。

以后。

Bug。

非常。

难查。

所以。

大型:

项目。

一般:

采用:


ViewModel

↓

Computed

↓

Selector

统一:

计算。

Watch。

只负责:


日志

埋点

网络

Toast

Dialog

这类:

副作用。


Watch 与 Vue watch 有什么区别?

很多人。

第一次。

学习。

HarmonyOS。

都会:

拿:

Vue。

比较。

其实。

思想。

类似。

但是。

实现。

不同。

Vue:


Reactive

↓

Effect

↓

Scheduler

HarmonyOS:


ObservedProperty

↓

Watcher

↓

Subscriber

Vue:

Watch。

可以:

监听:


a.b.c

HarmonyOS:

V1。

不能。

只能:

监听:

Property。

真正:

深层。

监听。

需要:


@Observed

@

ObjectLink

或者:

后面的:


Observed V2

@Watch 完整源码流程

执行:


this.count++

真正:

Runtime:

里面。

发生:

完整。

链路。


Setter

↓

ObservedProperty.set()

↓

Compare Old/New

↓

Watcher List

↓

Execute Watch Callback

↓

Subscriber List

↓

MarkDirty()

↓

Dirty Queue

↓

Scheduler

↓

VSync

↓

Build()

↓

ComponentTree

↓

Diff

↓

FrameNode

↓

RenderNode

↓

RenderService

↓

GPU

↓

屏幕

注意这里最关键的一点:

@Watch 永远不会直接刷新 UI。

它只是状态变化过程中的一个回调节点。真正驱动 UI 更新的,始终是 Subscriber → DirtyQueue → Scheduler → Build 这条响应式链路。

AppStorage 源码级深度解析(HarmonyOS 全局状态中心)

如果说:


@State

管理:

组件。

那么:


AppStorage

管理:

整个应用。

很多开发者第一次看到:


AppStorage.SetOrCreate("token", "123456")

都会认为:

AppStorage 就是一个 Map。

其实。

这只是表面。

真正 Runtime 里面。

AppStorage 是整个状态管理系统的:


Global Store

所有:


@StorageLink

@StorageProp

最终。

都会连接到:

这里。


为什么需要 AppStorage?

来看:

两个页面。


Login

↓

Home

登录。

拿到:


token

如果:

使用:


@State

那么:

Home。

根本:

拿不到。

因为:


State

↓

Component Scope

生命周期:

结束。

State。

就:

销毁。

所以。

需要:

Application。

级别。

的数据。


AppStorage 生命周期

真正:

Runtime。

里面。

更接近:


Application

↓

AppStorage

↓

Page

↓

Component

所以。

只要:

应用。

没有:

退出。


AppStorage

永远:

存在。

页面。

关闭。

不会:

销毁。


内部结构

很多人认为:

AppStorage。

就是:


std::map

其实。

更接近:


class AppStorage{

    unordered_map<
        string,
        ObservedProperty
    > values;

}

注意。

Value。

不是:


int

string

bool

而是:


ObservedProperty

所以。

它:

天然。

支持:

响应式。


SetOrCreate()

来看:


AppStorage.SetOrCreate(
    "token",
    "123456"
)

真正:

Runtime。

流程。


Find Key

↓

Exist?

↓

No

↓

Create ObservedProperty

↓

Insert Map

如果:

已经:

存在。

则:


ObservedProperty.set()

于是。

整个:

Subscriber。

全部:

通知。


为什么不是直接覆盖?

很多人。

觉得:


map[key]=value

就:

结束。

为什么:

还要:

ObservedProperty?

原因:

就在:

这里。

例如:

三个页面:


Home

Mine

Order

全部:

读取:


token

真正:

关系:


AppStorage

↓

token

↓

ObservedProperty

↓

Home

Mine

Order

以后:

登录:


AppStorage.Set(
    "token",
    "abc"
)

Runtime:

直接:


notify()

三个页面。

一起。

刷新。


Get()

开发者:


AppStorage.Get("token")

真正:

执行:


ObservedProperty.get()

于是:

依赖:

建立。

不是:

简单:

Map。

查询。


为什么 AppStorage 也是响应式?

来看:


Text(
AppStorage.Get("token")
)

Build。

执行。

真正:

Runtime。

记录:


token

↓

Text

以后:

修改:


token

UI:

自动。

刷新。

整个:

机制。

与:


@State

完全。

一样。


StorageCenter

真正:

HarmonyOS。

内部。

通常。

不会:

直接。

暴露:

Map。

而是:

一个:


StorageCenter

结构。

思想:

更接近:


StorageCenter

    │

    ├────AppStorage

    │

    ├────LocalStorage

    │

    ├────PersistentStorage

    │

    └────Environment

所有:

Storage。

最终。

都:

归:

StorageCenter。

统一:

管理。


为什么 AppStorage 查找很快?

真正:

Map。

结构:


unordered_map

例如:


token

↓

0x1000

查询:

时间。


O(1)

所以。

即使:

几百个:

Key。

性能。

依然。

很好。


为什么 Key 必须唯一?

例如:


AppStorage.Set(
"token",
"abc"
)

后来:


AppStorage.Set(
"token",
"def"
)

真正:

Map。

只有:


token

一个。

所以。

所有:

Subscriber。

都:

收到:


def

如果:

允许:

重复。

整个:

Subscriber。

关系。

全部。

乱掉。


AppStorage 为什么不能保存 Component?

例如:

很多新人:

喜欢:


AppStorage.Set(
"dialog",
this
)

实际上。

非常。

危险。

因为:

Component。

生命周期:


Create

↓

Destroy

而:

AppStorage。

生命周期:


Application

如果:

保存:

Component。

页面:

退出。

AppStorage。

仍然:

引用:

它。

最终:


Memory Leak

因此。

企业。

里面。

AppStorage。

只保存:


Token

Theme

Language

Config

UserInfo

Permission

不会:

保存:

UI。

对象。


@StorageProp 源码解析

终于。

来到:


@StorageProp("token")
token:string=""

很多人。

认为:

就是:


AppStorage.Get()

其实。

不是。

编译。

以后。

真正:

生成:


private __token

=
new StorageProp(

"token",

this

)

注意。

这里。

又:

不是:


ObservedProperty

而是:


StorageProp

职责:

就是:

连接:

AppStorage。


初始化流程

页面:

Create。

真正:

执行:


StorageProp

↓

Find AppStorage

↓

token

↓

Bind

↓

Subscriber

以后:

AppStorage:

修改。

StorageProp。

立即:

收到:

通知。


为什么 StorageProp 不能修改?

例如:


this.token="abc"

很多版本。

直接:

警告。

为什么?

因为:

StorageProp。

本质。

就是:


Readonly

真正:

Setter。

更接近:


throw Error()

必须:

修改:


AppStorage.Set(...)

才能:

更新。

所有。

页面。

这与 @Prop 的设计思想一致:消费者只能读取,不负责修改源数据。


@StorageLink 源码解析

继续。

来看:


@StorageLink("token")

token:string=""

很多人。

觉得。

只是:

StorageProp。

升级。

其实。

真正。

生成:


StorageLinkProperty

而:

不是:

StorageProp。

它:

内部:

保存:


AppStorage

ObservedProperty

真正:

关系:


Component

↓

StorageLink

↓

ObservedProperty

↓

AppStorage

于是:

执行:


this.token="456"

真正:

变成:


StorageLink.set()

↓

AppStorage.set()

↓

notify()

↓

全部页面刷新

为什么 StorageLink 可以双向同步?

例如:

页面 A:


@StorageLink("theme")
theme:string=""

页面 B:


@StorageLink("theme")
theme:string=""

A:

执行:


theme="Dark"

真正:

Runtime:


A

↓

StorageLink

↓

AppStorage

↓

ObservedProperty

↓

notify

↓

B

↓

Build

整个:

应用。

同步。

完成。

这就是:

HarmonyOS。

全局。

响应式。


AppStorage 为什么比 EventBus 更好?

很多项目:

以前:

这样:

写:


Login

↓

EventBus

↓

Home

↓

Mine

↓

Order

问题:

越来越:

严重。

例如:


没人知道:

谁发

谁收

而:

AppStorage:

真正:

依赖:

明确。

例如:


theme

↓

Home

Mine

Setting

Subscriber。

全部。

可追踪。

所以。

大型。

项目。

几乎。

都会:

使用:

Storage。

而不是:

EventBus。


AppStorage 完整运行流程

执行:


AppStorage.Set(
"token",
"abc"
)

Runtime。

完整。

链路:


AppStorage

↓

Find Key

↓

ObservedProperty.set()

↓

Watcher

↓

SubscriberManager

↓

MarkDirty()

↓

DirtyQueue

↓

Scheduler

↓

Build()

↓

Diff

↓

FrameNode

↓

RenderNode

↓

RenderService

↓

GPU

↓

屏幕刷新

可以发现,AppStorage 本质上就是一个全局的 ObservedProperty 容器。它并没有发明新的刷新机制,而是把 @State 的响应式能力提升到了应用级别。


LocalStorage 与 AppStorage 的本质区别

很多人只记 API:


AppStorage

LocalStorage

但源码层面的区别只有一句话:


AppStorage
↓

Application 生命周期

LocalStorage
↓

Page 生命周期

也就是说:

  • AppStorage:应用不退出,就一直存在。
  • LocalStorage:页面销毁,对应的数据和订阅关系也会一起释放。
  • 两者底层都建立在 ObservedProperty + SubscriberManager 之上,只是所属的 Storage 容器不同。

LocalStorage 源码级深度解析(页面级状态容器)

前面我们分析了:


AppStorage

它的生命周期是:


Application

那么:


LocalStorage

生命周期则完全不同。

它属于:


Page

很多开发者把它理解成:

AppStorage 的缩水版。

其实。

源码里面。

它们除了生命周期。

还有很多区别。


为什么需要 LocalStorage?

来看一个例子。

首页:


Home

商品页:


Goods

订单页:


Order

三个页面。

都有:


loading

page

keyword

selectedTab

这些状态。

有没有必要:

放:


AppStorage

没有。

因为:

离开页面。

这些数据。

就没用了。

所以:

HarmonyOS。

提供:


LocalStorage

生命周期

真正:

Runtime。

关系:


Application

        │

        ▼

Navigation

        │

 ┌──────┴───────┐

 ▼              ▼

PageA        PageB

 │              │

 ▼              ▼

LocalStorageA LocalStorageB

注意。

每一个:

Page。

都有:

自己的:


LocalStorage

互不影响。


创建流程

页面:

Push。

真正:

Runtime。

执行:


Create Page

↓

Create LocalStorage

↓

Bind Context

↓

Create Component

所以:

Component。

可以:

共享:

当前:

Page。

LocalStorage。


内部结构

其实。

和:

AppStorage。

几乎一样。


class LocalStorage{

    unordered_map<
        string,
        ObservedProperty
    > values;

}

真正:

不同。

只有:

Owner。


AppStorage

↓

Application

LocalStorage

↓

Page

为什么页面关闭自动释放?

假设:

Navigation。

执行:


router.back()

真正:

Runtime。

流程。


Destroy Component

↓

Destroy Subscriber

↓

Destroy LocalStorage

↓

Erase Map

↓

Release Memory

所以。

页面。

退出。

LocalStorage。

自然。

不存在。


为什么不会内存泄漏?

很多开发者:

以前。

自己:

写:


const pageData={}

放:

全局。

结果:

页面。

关闭。

数据。

一直。

还在。

HarmonyOS。

不会。

因为:

真正:

Runtime。

里面。

LocalStorage。

属于:


PageContext

Page。

Destroy。

整个:

Storage。

一起:

释放。


为什么 LocalStorage 查找更快?

AppStorage:

真正:

路径:


Component

↓

Application

↓

StorageCenter

↓

AppStorage

↓

Map

而:

LocalStorage。

只需要:


Component

↓

PageContext

↓

LocalStorage

少:

一级。

所以:

页面。

内部。

频繁。

状态。

建议:

LocalStorage。


@LocalStorageLink 源码

开发者:


@LocalStorageLink("page")
page:number=1

真正:

编译:

生成:


private __page

=

LocalStorageLinkProperty

真正:

初始化:


Find PageContext

↓

Find LocalStorage

↓

Find Key

↓

Bind Subscriber

以后。

修改:


page++

真正:

执行:


LocalStorage

↓

ObservedProperty

↓

notify

↓

Build

多个组件共享

来看:


Page

 │

 ├────Header

 │

 ├────List

 │

 └────Footer

三个。

组件。

全部:


@LocalStorageLink("keyword")

真正:

关系:


LocalStorage

↓

keyword

↓

ObservedProperty

↓

Header

List

Footer

Header。

修改:


keyword

List。

立即。

刷新。

Footer。

立即。

刷新。

但是。

别的页面。

不知道。


LocalStorage 为什么不能跨页面?

例如:

PageA:


keyword=HarmonyOS

跳转:


PageB

PageB。

读取:


keyword

得到:


undefined

为什么?

因为:

真正:

Storage。

不同。


PageA

↓

LocalStorageA

PageB

↓

LocalStorageB

没有:

任何。

关系。


LocalStorage 与 Navigation

真正:

HarmonyOS。

Navigation。

里面。

每一个:

Destination。

都会:

创建:

自己的:


PageContext

例如:


Navigation

 │

 ├────Home

 │       │

 │       ▼

 │   LocalStorageA

 │

 └────Detail

         │

         ▼

    LocalStorageB

所以。

Navigation。

天然。

支持:

页面。

隔离。


为什么 Dialog 也能拥有 LocalStorage?

很多人。

不知道。

真正:

Dialog。

其实。

也有:

自己的:


Context

例如:


Page

 │

 ├────Content

 │

 └────Dialog

        │

        ▼

 DialogContext

因此。

Dialog。

完全。

可以:

拥有:

自己的:

Storage。

关闭。

Dialog。

Storage。

立即。

释放。


LocalStorage 与 @Provide

很多人。

容易。

混淆。

其实。

区别。

很大。

Provide:

真正:

关系:


Component Tree

↓

Lookup

LocalStorage:

真正:

关系:


Page

↓

Storage

↓

Key

Provide:

按:

组件树。

查找。

LocalStorage:

直接:

Map。

查询。

所以:

速度:

更快。


为什么 LocalStorage 更适合页面状态?

例如:

商城。


keyword

page

filter

sort

loading

refresh

currentTab

全部:

都是:

页面。

退出。

即:

销毁。

如果:

放:

AppStorage。

越来越:

大。

最终:


Application

↓

300+

Key

管理:

困难。

企业。

一般:

这样:

划分:


AppStorage

↓

User

Theme

Language

Token

Permission

LocalStorage:


GoodsPage

↓

page

keyword

sort

filter

loading

职责。

非常:

清晰。


PersistentStorage 为什么出现?

前面:

两个:

Storage。

都有:

一个:

问题。

应用:

退出。

数据。

全部。

消失。

例如:


Theme

Language

RememberPassword

这些。

希望:

下次。

启动。

仍然:

存在。

于是:

HarmonyOS。

增加:


PersistentStorage

真正:

结构。

变成:


StorageCenter

       │

 ┌─────┼──────────┐

 ▼     ▼          ▼

App Local Persistent

PersistentStorage 底层到底是什么?

很多教程。

都会说:


PersistentStorage

↓

Preferences

其实。

并不完整。

真正。

Runtime。

关系:


PersistentStorage

↓

ObservedProperty

↓

Preferences

↓

File

注意。

真正:

UI。

读取。

不是:

Preferences。

而是:


ObservedProperty

Preferences。

只是:

最终:

持久化。

介质。


初始化流程

应用:

启动。

真正:

Runtime。

执行:


Preferences.open()

↓

Read File

↓

Create ObservedProperty

↓

Insert PersistentStorage

以后。

UI。

读取:

直接:

访问:

内存。

不是:

文件。

因此。

速度。

非常。

快。


为什么修改不会立即写磁盘?

例如:

输入:


HarmonyOS

十个:

字符。

如果:

每次。

都:


write()

flush()

SSD。

寿命。

都会:

下降。

真正:

Runtime。

采用:


Memory

↓

Delay

↓

Batch Write

↓

Preferences

企业。

一般。

还会:

增加:


500ms

Debounce

避免:

频繁:

IO。


PersistentStorage 更新流程

执行:


PersistentStorage.Set(
"theme",
"Dark"
)

真正:

流程:


ObservedProperty

↓

notify()

↓

UI Refresh

↓

Async Task

↓

Preferences.put()

↓

Flush File

注意:

UI 刷新优先,磁盘写入异步进行。

因此。

用户。

不会。

感觉:

卡顿。


四大 Storage 关系图

真正:

HarmonyOS。

Runtime。

里面。

最终。

统一。

管理:


                 StorageCenter

                      │

      ┌───────────────┼────────────────┐

      ▼               ▼                ▼

 AppStorage      LocalStorage    PersistentStorage

      │               │                │

      ▼               ▼                ▼

 ObservedProperty ObservedProperty ObservedProperty

      │               │                │

      └───────────────┴────────────────┘

                 SubscriberManager

                        │

                        ▼

                    Dirty Queue

                        │

                        ▼

                    Scheduler

                        │

                        ▼

                      Build()

                        │

                        ▼

                     RenderService

可以看到:

无论:

是哪一种:

Storage。

最终。

都会:

回到:

同一套:

响应式调度系统。

区别。

只是:

生命周期。

和:

数据来源。

Environment 源码级深度解析(HarmonyOS 系统环境响应式)

前面我们分析了:


@State

AppStorage

LocalStorage

PersistentStorage

这些。

都是:

开发者主动修改。

但是:

还有一种状态。

不是开发者修改。

而是:

系统修改。

例如:


深色模式

横竖屏

字体大小

语言

地区

24小时制

屏幕方向

窗口大小

SafeArea

用户:

打开:

系统设置。

整个应用。

立即刷新。

为什么?

因为:

HarmonyOS Runtime。

还有:

一个:


Environment

它负责:

整个:

系统环境。

响应式。


什么是 Environment?

很多人认为:

Environment。

就是:


Configuration

其实。

不是。

Configuration。

只是:

数据。

Environment。

才是:

响应式。

容器。

真正:

关系:


System

↓

Configuration

↓

Environment

↓

ObservedProperty

↓

UI

所以。

Environment。

本质:

也是:

一个:


Storage

只是:

数据来源:

不是:

开发者。

而是:

系统。


Runtime 启动流程

应用:

启动。

真正:

执行:


Application

↓

WindowStage

↓

Environment

↓

Register System Listener

↓

Ready

这里:

最重要:

一步。

就是:


Register System Listener

注册系统监听

真正:

Runtime。

更接近:


Environment::Init(){

registerDarkMode();

registerLanguage();

registerOrientation();

registerFontScale();

registerSafeArea();

}

系统。

变化。

以后。

都会:

进入:

Environment。


内部结构

真正:

Environment。

思想。

更接近:


class Environment{

unordered_map

<string,

ObservedProperty>

properties;

}

例如:


{

"colorMode",

"language",

"fontScale",

"orientation",

"layoutDirection"

}

看到没有。

又出现:


ObservedProperty

说明。

整个:

HarmonyOS。

状态。

最终。

都是:

同一套。

机制。


深色模式为什么自动刷新?

例如:

代码:


@Environment("colorMode")

colorMode:ColorMode

真正:

初始化:


Find Environment

↓

Find colorMode

↓

Bind Subscriber

之后。

用户:

打开:


设置

↓

深色模式

系统:

发送:


ColorModeChanged

Runtime:

收到:


System Event

↓

Environment.set()

↓

notify()

↓

Build()

页面:

立即:

刷新。


为什么不用轮询?

很多新人:

会问:

是不是:

每隔:

100ms。

检查:

一次?

当然。

不是。

真正:

系统。

采用:


Observer

例如:


DisplayManager

↓

Callback

↓

Environment

只有:

发生:

变化。

才:

通知。

因此:

CPU。

几乎:

没有:

消耗。


DarkMode 流程

整个:

链路:

其实:

就是:


Settings

↓

DisplayManager

↓

WindowManager

↓

Environment

↓

ObservedProperty

↓

Subscriber

↓

Dirty Queue

↓

Scheduler

↓

Build

↓

RenderService

注意。

真正。

没有:

任何:

轮询。


多语言为什么不用重启?

来看:

系统:

切换:


中文

↓

English

很多。

App。

以前。

必须:

重启。

HarmonyOS。

不用。

为什么?

真正:

流程:


Language Changed

↓

Configuration

↓

Environment

↓

language

↓

notify()

↓

Text

↓

重新读取 ResourceManager

↓

重新布局

重点:

不是:

重新:

创建:

页面。

而是:

重新:

Build。


为什么文字自动变化?

例如:


Text(
$r("app.string.login")
)

Build。

期间:

真正:

执行:


ResourceManager

↓

Current Language

↓

Find Resource

↓

String

Language。

变化。

重新:

Build。

ResourceManager。

重新:

读取。

于是。

文字。

自动:

变成:

英文。


FontScale 为什么能全部变大?

用户:

设置:


字体

100%

↓

125%

真正:

Environment。

更新:


fontScale

所有:

依赖:


fontScale

组件:

全部:

Dirty。

例如:


Text()

Button()

TextInput()

List()

Navigation()

重新:

Measure。

最终。

重新:

Layout。


为什么不仅仅 Build?

很多人。

认为:

字体:

变大。

只需要:

重新:

Text。

其实。

不是。

真正:

流程:


fontScale

↓

Build

↓

Measure

↓

Layout

↓

Render

因为:

Text。

尺寸。

发生:

变化。

整个:

布局。

都要:

重新。

计算。


Orientation 为什么整个页面旋转?

例如:

手机:


竖屏

↓

横屏

真正:

系统:

发送:


OrientationChanged

Runtime:

执行:


Environment

↓

orientation

↓

notify

↓

Window

↓

Measure

↓

Layout

↓

Render

真正:

变化。

不是:

UI。

而是:

Window。

尺寸。


SafeArea 为什么自动更新?

例如:

折叠屏。

展开。


SafeArea

↓

Changed

Environment。

收到:


Insets Changed

所有:

依赖:

SafeArea。

组件。

重新:

Layout。

所以。

开发者。

不用:

自己。

计算。


WindowSize 为什么自动适配?

HarmonyOS。

支持:


Phone

Tablet

PC

Fold

Car

窗口。

大小。

变化。

真正:

流程:


WindowManager

↓

Environment

↓

WindowSize

↓

Layout Constraint

↓

Measure

↓

Layout

因此。

ArkUI。

天然:

支持:

响应式。

布局。


为什么 Environment 不允许修改?

例如:


colorMode=Dark

实际上。

直接:

报错。

为什么?

因为:

真正。

Owner。

不是:

开发者。

而是:


System

Environment。

只有:


Getter

没有:

真正:

Setter。

Setter。

只有:

System。

可以:

调用。


Environment 与 AppStorage 区别

很多人。

第一次。

学习。

容易:

混。

真正:

区别:


AppStorage

↓

Developer

开发者:

修改。


Environment

↓

System

系统:

修改。

但是:

底层:

统一:

都是:


ObservedProperty

因此。

刷新。

流程。

完全:

一致。


Environment 为什么性能这么高?

真正:

Runtime。

维护:

依赖图:


Environment

 │

 ├────colorMode

 │

 ├────language

 │

 ├────fontScale

 │

 └────orientation

每个:

Property。

都有:

自己的:

Subscriber。

例如:


fontScale

↓

TextA

TextB

修改:


fontScale

不会:

通知:


language

因此。

粒度:

非常。

细。


Runtime 如何处理多个系统事件?

例如:

用户:

同时:

切换:


Dark

Language

FontScale

如果:

每个。

都:

Build。

一次。

性能:

非常。

差。

真正:

Runtime。

采用:


Batch Update

例如:


Dark

↓

Dirty

Language

↓

Dirty

FontScale

↓

Dirty

统一:

进入:


Dirty Queue

下一帧:

一起:

Build。

这就是:

ArkUI。

一直:

强调:


Frame Scheduler

原因。


Environment 完整源码流程

例如:

用户:

打开:


深色模式

真正:

Runtime:

里面:

完整:

链路:


System Settings

↓

DisplayManager

↓

WindowManager

↓

Configuration

↓

Environment

↓

ObservedProperty.set()

↓

Watcher

↓

SubscriberManager

↓

MarkDirty()

↓

Dirty Queue

↓

Frame Scheduler

↓

Build()

↓

Measure()

↓

Layout()

↓

Diff()

↓

FrameNode

↓

RenderNode

↓

RenderService

↓

GPU

↓

屏幕刷新

可以发现:

Environment 并不是一套特殊的状态管理。

它只是把系统事件转换成了 ObservedProperty,然后复用了整个 ArkUI 的响应式调度体系。


Runtime 状态管理架构总览(第一阶段总结)

经过前面的章节,我们已经把 HarmonyOS V1 状态管理的核心 Runtime 基本串起来了:


                    State Management Runtime

                                │
        ┌───────────────────────┼────────────────────────┐
        │                       │                        │
        ▼                       ▼                        ▼
    @State                 AppStorage            Environment
        │                       │                        │
        ▼                       ▼                        ▼
   ObservedProperty      ObservedProperty       ObservedProperty
        │                       │                        │
        └───────────────────────┼────────────────────────┘
                                │
                                ▼
                      SubscriberManager
                                │
                                ▼
                          Dirty Queue
                                │
                                ▼
                        Frame Scheduler
                                │
                                ▼
                             Build()
                                │
                                ▼
                         Component Tree
                                │
                                ▼
                             Diff()
                                │
                                ▼
                           FrameNode
                                │
                                ▼
                           RenderNode
                                │
                                ▼
                         RenderService
                                │
                                ▼
                                GPU

到这里,其实我们分析的还只是 HarmonyOS 状态管理 V1。

HarmonyOS 状态管理 V2 源码深度解析(为什么官方要重写整个状态管理系统)

很多开发者都有一个疑问:

HarmonyOS 已经有:


@State
@Prop
@Link
@Provide
@Consume
@Observed
@ObjectLink

为什么 HarmonyOS 5 又推出:


@ObservedV2
@Trace

甚至官方文档都开始推荐:


V2 State Management

是不是仅仅为了换一个 API?

答案当然不是。

真正原因:

V1 在 Runtime 架构上已经遇到了瓶颈。


V1 最大的问题

来看一个例子。


@Observed
class User {

    name: string = "Tom"

    age: number = 18

    phone: string = "13800000000"

    address: Address = new Address()

}

UI:


Text(user.name)

如果:

修改:


user.phone = "13999999999"

理论上:

只有:


phone

变化。

但是:

V1 Runtime。

很多情况下。

仍然会:

通知:

整个:


User

对应:

所有:

Subscriber。

为什么?

因为:

V1。

真正维护的是:


Object Level Dependency

对象级依赖。

不是:

属性级依赖。


V1 Dependency Graph

真正:

更接近:


User

     │

     ▼

Subscriber

     │

     ▼

Component

Runtime。

知道:


User

变化

但是:

不知道:

到底:

哪一个:

Property。

变化。

因此。

很多:

无意义:

刷新。


V2 为什么重新设计?

HarmonyOS Runtime。

希望做到:

真正:


Property Dependency

例如:

真正:

建立:


User

├────name

├────age

├────phone

└────address

每一个:

Property。

都有:

自己的:

Subscriber。

例如:


name

↓

TextA

phone

↓

TextB

修改:


phone

真正:

只刷新:


TextB

V2 真正变化是什么?

很多人:

认为:

V2。

只是:


@Observed

↓

@ObservedV2

其实。

真正:

变化:

不是:

API。

而是:

Runtime。

整个:

Dependency Graph。

重写。


V1 Runtime

真正:

关系:


ObservedObject

↓

Proxy

↓

Subscriber

Proxy。

负责:

所有。

Property。

通知。


V2 Runtime

真正:

变成:


ObservedV2

↓

TraceProperty

↓

PropertyNode

↓

DependencyGraph

↓

Subscriber

是不是:

复杂:

很多?

没错。

因为:

真正:

增加:

了一层:


PropertyNode

PropertyNode 是什么?

Runtime。

里面。

更接近:


class PropertyNode{

    string name;

    vector<Subscriber*> subscribers;

}

例如:

User。

真正:

变成:


User

 │

 ├────PropertyNode(name)

 │

 ├────PropertyNode(age)

 │

 ├────PropertyNode(phone)

 │

 └────PropertyNode(address)

不是:

一个:

Subscriber。

而是:

四个。


@Trace 到底是什么?

来看:


@ObservedV2
class User{

    @Trace
    name:string="Tom"

    @Trace
    age:number=18

}

很多教程:

一句话:


Trace

负责监听

其实。

真正。

作用:

不是:

监听。

而是:

告诉:

编译器:


Generate PropertyNode

没有:


@Trace

Runtime。

不会:

创建:

PropertyNode。


编译器到底生成了什么?

开发者:


@Trace
name="Tom"

真正:

编译:

更接近:


registerTrace(

"name",

offsetof(User,name)
)

也就是说。

编译阶段。

已经:

把:

所有:

Trace。

收集。

完成。

Runtime。

启动。

不用:

再次:

扫描。


为什么 V2 更快?

V1。

真正:

初始化:


Create Object

↓

Runtime Scan

↓

Proxy

V2:

真正:


Compile Time

↓

Generate Metadata

↓

Runtime Read Metadata

大量:

工作。

提前:

到:

编译。

所以:

Runtime。

越来越:

轻。


Metadata

真正:

编译。

以后。

Class。

里面。

多出:

类似:


Metadata{

PropertyCount

PropertyOffset

PropertyType

TraceFlag

}

Runtime。

直接:

读取:

Metadata。

不用:

反射。


Dependency Graph

真正:

Runtime。

维护:

一个:

巨大的:


Dependency Graph

例如:


User.name

↓

Text1

User.age

↓

Text2

Order.price

↓

Text3

整个:

App。

所有:

Property。

都:

连接:

这里。


Build 阶段发生什么?

来看:


Text(user.name)

Build。

期间。

Runtime。

真正:

执行:


Read Property

↓

Find PropertyNode

↓

Current Component

↓

Register Subscriber

于是:

真正:

关系:

建立。


修改发生什么?

执行:


user.name="Jack"

真正:

Runtime。

流程:


PropertyNode(name)

↓

Subscriber

↓

MarkDirty

↓

Scheduler

注意。

不是:

整个:

User。

而是:


name

V1 VS V2

真正:

区别:

V1:


User

↓

Subscriber

V2:


User

↓

name

↓

Subscriber

V1:


phone

变化

↓

User刷新

V2:


phone

变化

↓

Phone Subscriber

这就是:

真正:

性能:

提升。

来源。


为什么列表性能提升巨大?

例如:

1000 条:

商品。


Goods

↓

1000 Item

每一个:

Item:

都有:


price

stock

title

image

V1。

修改:


stock

很多:

Item。

仍然:

重新:

Build。

V2。

真正:


stock

↓

StockNode

↓

Current Item

其他:

999 个。

完全:

不动。

所以。

官方。

测试。

大型:

List。

性能。

提升:

非常:

明显。


为什么 V2 不再依赖大量 Proxy?

V1:

对象。

几乎:

全部:

Proxy。

Proxy。

创建。

本身:

有:

成本。

V2。

真正:

很多:

信息。

来自:

编译期。

Runtime。

Proxy。

数量。

明显:

减少。

GC。

压力。

也:

下降。


Runtime 初始化

真正:

V2。

启动。

流程:


Load Metadata

↓

Create PropertyNode

↓

Create DependencyGraph

↓

Register Component

↓

Ready

而不是:

运行:

时。

不停:

扫描:

Class。


V2 完整刷新流程

执行:


user.name="HarmonyOS"

真正:

Runtime:

发生:

完整:

链路:


Setter

↓

PropertyNode(name)

↓

Compare Old/New

↓

Watcher

↓

DependencyGraph

↓

SubscriberManager

↓

MarkDirty()

↓

DirtyQueue

↓

FrameScheduler

↓

Build()

↓

Diff()

↓

FrameNode

↓

RenderNode

↓

RenderService

↓

GPU

↓

屏幕刷新

这里可以看到,V2 的核心变化并不是刷新流程,而是 DependencyGraph 的粒度发生了变化。

V1 是:


Object → Subscriber

V2 是:


Property → Subscriber

这也是 HarmonyOS V2 能够实现真正细粒度响应式的根本原因。


V2 Runtime 总架构

整个 HarmonyOS 5 状态管理 Runtime 可以抽象成下面这一张图:


                    Compiler

                        │

                        ▼

                 Generate Metadata

                        │

                        ▼

                PropertyNode Table

                        │

                        ▼

                Dependency Graph

                        │

                        ▼

              SubscriberManager

                        │

                        ▼

                  Dirty Queue

                        │

                        ▼

                Frame Scheduler

                        │

                        ▼

                    Build()

                        │

                        ▼

                     Diff()

                        │

                        ▼

                  FrameNode Tree

                        │

                        ▼

                   RenderService

                        │

                        ▼

                        GPU

Build() 源码级深度解析(HarmonyOS ArkUI 渲染入口)

前面,我们已经把整个状态管理分析完了。

真正的数据流最终都会来到这里:


@State

↓

ObservedProperty

↓

SubscriberManager

↓

DirtyQueue

↓

FrameScheduler

↓

Build()

很多开发者都有一个误区:

build() 就是创建 UI。

其实,这句话只对了一半。

真正来说,build() 不是绘制 UI,而是生成 UI 描述(UI Description)。

真正绘制发生在后面的 RenderService。

这一章,我们正式进入 ArkUI Runtime 最核心的部分。


一个最简单的 build()

例如:


@Entry
@Component
struct Index {

  @State
  count: number = 0

  build() {
    Column() {
      Text(this.count.toString())

      Button("+")
        .onClick(() => {
          this.count++
        })
    }
  }
}

很多人觉得:

build()

执行以后:


Text

↓

GPU

其实。

完全不是。

真正发生的是:


build()

↓

Component Tree

↓

FrameNode Tree

↓

Diff

↓

RenderNode

↓

RenderService

↓

GPU

Build 到底返回什么?

很多前端框架:

build()

都会:

返回:

Virtual DOM。

HarmonyOS。

不是。

真正:

build。

生成:


FrameNode

不是:

HTML。

不是:

DOM。

不是:

Canvas。

而是:

ArkUI。

自己的:

UI Tree。


Runtime 为什么需要 FrameNode?

来看:


Column() {

    Text("A")

    Button("B")

}

真正:

Runtime。

不会:

立即:

创建:

GPU。

对象。

而是:

先:

创建:


FrameNode

↓

ColumnNode

↓

TextNode

↓

ButtonNode

形成:

树。

例如:


Column

├────Text

└────Button

整个:

树。

就是:

FrameNode Tree。


FrameNode 到底是什么?

很多教程:

一句话:


FrameNode

↓

节点

其实。

远远:

不够。

真正:

源码。

更接近:


class FrameNode{

NodeId id;

Pattern* pattern;

LayoutProperty* layout;

PaintProperty* paint;

EventHub* event;

GeometryNode* geometry;

vector<FrameNode*> children;

}

一个:

FrameNode。

里面。

几乎。

保存:

所有。

UI。

信息。


为什么叫 FrameNode?

因为。

它。

描述:

的是:

一帧。

UI。

真正。

状态。

不是:

永久。

对象。

下一帧。

可能:

重新:

Diff。

更新。

所以。

叫:


FrameNode

不是:

View。


Build 真正流程

真正:

Runtime。

更接近:


Build()

↓

Create FrameNode

↓

Set Property

↓

Append Child

↓

Finish Tree

例如:


Text("Hello")

真正:

Runtime。

执行:


FrameNode*

node=

CreateTextNode();

node->content="Hello";

不是:

直接:

画。

文字。


为什么 Build 不允许写耗时逻辑?

例如:


build(){

request()

sleep()

for(10000000)

}

很多新人。

觉得。

没问题。

实际上。

非常。

危险。

因为。

真正:

Runtime。

执行:


VSync

↓

Build

↓

Layout

↓

Render

如果:

Build。

阻塞:

200ms。

整个。

16ms。

帧率。

直接:

掉。

最终:


60FPS

↓

5FPS

所以。

官方。

一直:

强调:


Build

必须:

纯函数

Build 为什么不能修改 State?

例如:


build(){

count++

}

发生:

什么?

真正:

流程:


Build

↓

Setter

↓

Dirty

↓

Build

↓

Setter

↓

Dirty

↓

Build

无限。

递归。

所以。

Runtime。

里面。

真正。

维护:


bool building;

Build。

期间。

再次:

修改:

State。

直接:

Warning。

甚至:

Crash。


Build 为什么可以读取 State?

来看:


Text(this.count.toString())

真正:

执行:

不是:

简单:

Getter。

而是:


ObservedProperty.get()

↓

Dependency Collect

↓

Subscriber Register

Build。

真正:

最大的:

工作。

就是:

建立:

依赖。

关系。

不是:

创建:

UI。


Build Dependency

真正:

Runtime。

维护:


Current Building Component

例如:


ComponentA

读取:


count

真正:

建立:


count

↓

ComponentA

以后:

修改:


count

Runtime。

直接:

知道:

应该:

刷新:

谁。


为什么没有读取就不会刷新?

例如:


@State

count

@State

age

Build。

里面:

只有:


Text(count)

真正:

Dependency:

只有:


count

修改:


age

根本。

没人。

依赖。

所以。

Build。

不会:

执行。


Component Tree

很多人。

容易:

混。

真正:

ArkUI。

有:

两棵:

树。

第一棵:


Component Tree

例如:


Home

↓

Header

↓

List

↓

Item

第二棵:


FrameNode Tree

例如:


Column

↓

List

↓

Text

注意。

Component。

负责:

逻辑。

FrameNode。

负责:

UI。


为什么需要两棵树?

例如:

一个:

组件:


GoodsItem

里面:

真正:

可能:

生成:


Column

├──Image

├──Text

├──Button

所以:

一个:

Component。

对应:

多个:

FrameNode。

不是:

一一。

对应。


Pattern 是什么?

真正:

FrameNode。

里面。

最重要:

成员。

就是:


Pattern*

例如:

Button。

真正:

Pattern。

更接近:


ButtonPattern

Text。

真正:


TextPattern

List。

真正:


ListPattern

Pattern。

负责:

组件。

行为。

例如:

Button:

点击。

Hover。

Focus。

LongPress。

全部:

Pattern。

处理。

不是:

FrameNode。


GeometryNode

真正:

Layout。

全部:

保存:

这里。

例如:


x

y

width

height

还有:


margin

padding

border

Layout。

结束。

GeometryNode。

全部:

计算。

完成。


LayoutProperty

例如:


.width(100)

.height(50)

.padding(20)

真正:

保存:


LayoutProperty

不是:

Geometry。

为什么?

因为:

Geometry。

是真实。

尺寸。

LayoutProperty。

只是:

配置。

例如:


width=100

真正:

布局。

可能:

变成:


width=95

因为:

父布局:

约束。


PaintProperty

例如:


.backgroundColor()

.opacity()

.shadow()

.borderRadius()

真正:

全部:

保存:


PaintProperty

Layout。

不用:

关心。

颜色。

Paint。

阶段。

才:

读取。

真正。

实现:

职责:

分离。


EventHub

例如:


.onClick()

.onTouch()

.onHover()

真正:

不会:

直接:

绑定:

FrameNode。

而是:

统一:

放:


EventHub

真正:

点击:

流程:


Touch

↓

HitTest

↓

EventHub

↓

Callback

所以:

FrameNode。

不用:

知道:

事件。

实现。


Build 完整流程

执行:


build()

真正:

Runtime:

完整:

流程:


FrameScheduler

↓

Build()

↓

Create Component Tree

↓

Create FrameNode

↓

Register Dependency

↓

Append Child

↓

Finish Tree

↓

Diff

↓

Layout

↓

Paint

↓

RenderNode

↓

RenderService

↓

GPU

这里:

Build。

真正。

负责:

的是:

描述 UI。

不是:

绘制 UI。


为什么 Build 很快?

因为:

Build。

只是:

创建:

数据。

例如:


FrameNode

Property

Dependency

真正:

GPU。

真正:

Raster。

全部:

后面:

完成。

所以。

Build。

可以:

控制:

在:

几毫秒。

完成。


Build 与 React Render 的区别

React:


Render

↓

Virtual DOM

↓

Diff

↓

DOM

HarmonyOS:


Build

↓

FrameNode

↓

Diff

↓

RenderNode

最大的区别:

HarmonyOS 没有浏览器 DOM。

FrameNode 本身就是 ArkUI Runtime 的内部 UI 树,因此比 Web DOM 少了一层抽象,也减少了对象转换和桥接开销。


Build() 在 ArkUI 中的位置

整个 Runtime 可以抽象成下面这一张图:


State Changed

↓

ObservedProperty

↓

SubscriberManager

↓

DirtyQueue

↓

FrameScheduler

↓

Build()

↓

FrameNode Tree

↓

Diff()

↓

Layout()

↓

Measure()

↓

Paint()

↓

RenderNode

↓

RenderService

↓

GPU

↓

Display

到这里,我们已经真正进入了 ArkUI 渲染引擎。

前面所有状态管理(@State、@Observed、AppStorage、Environment)最终都会汇聚到 Build(),然后开始进入渲染阶段。

Diff 算法源码级深度解析(HarmonyOS 为什么刷新一个 Text 不会重绘整个页面)

前面,我们已经分析到:


State Changed

↓

Build()

↓

FrameNode Tree

很多开发者都有一个疑问。

例如:


Text(this.count.toString())

修改:


this.count++

为什么:

只有:


Text

刷新?

而不是:

整个:


Column

重新:

创建?

真正答案:

就在:


Diff

什么是 Diff?

很多教程。

一句话:

Diff 就是比较。

其实。

远远:

不够。

真正:

ArkUI。

Diff。

负责:


Old FrameNode

↓

Compare

↓

New FrameNode

↓

Generate Patch

最后。

真正:

修改:

只有:

变化:

节点。

不是:

全部:

重新:

创建。


一个最简单例子

第一次:

Build:


Column() {

    Text("A")

    Button("OK")

}

生成:

树:


Column

├──Text

└──Button

第二次:

Build:


Column() {

    Text("B")

    Button("OK")

}

真正:

Diff。

发现:


Column

一样

Button:


一样

只有:


Text Content

变化

于是:

Patch:


Update Text

结束。


为什么不用全部重新创建?

如果:

重新:

Create:


Column

↓

Text

↓

Button

每秒:

60 帧。

整个:

CPU。

直接:

爆炸。

真正:

Runtime。

永远:

希望:


Reuse

不是:


Recreate

Diff 的真正输入

很多人。

认为:

Diff。

比较:

UI。

其实。

不是。

真正:

比较:


Old FrameNode Tree

↓

New FrameNode Tree

不是:

屏幕。

更不是:

GPU。

GPU。

甚至:

不知道:

Diff。

存在。


FrameNode Identity(节点身份)

来看:


Text("A")

第一次:

Build:

真正:

Runtime:

生成:


NodeId

1001

第二次:

Build:

如果:

还是:


Text

Runtime:

希望:

继续:

使用:


1001

不是:

创建:


1002

为什么?

因为:

Node。

里面:

保存:

大量:

数据。

例如:


Layout

Animation

Focus

Gesture

Event

RenderNode

全部:

复用。

性能。

极高。


NodeId 如何生成?

真正:

Runtime。

更接近:


NodeId++

↓

1001

1002

1003

但是:

Build。

再次:

执行。

不能:

重新:

编号。

否则:

Diff。

完全:

失效。

所以。

真正:

使用:


ElementId

而不是:

简单:

NodeId。


ElementId 到底是什么?

很多教程:

从来:

没有:

讲。

实际上:

真正:

Runtime。

更接近:


Component Path

+

Index

+

Key

例如:


Home

↓

Column

↓

Text

真正:

ElementId:

可能:


Home/0/Text

第二次:

Build。

仍然:

得到:

同一个:

ElementId。

于是:

Diff。

知道:

这是:

同一个:

节点。


为什么顺序变化就麻烦?

来看:

第一次:


Column() {

Text("A")

Button()

}

第二次:


Column() {

Button()

Text("A")

}

如果:

没有:

Key。

真正:

Diff:

看到:


Index0

↓

Text

↓

Button

认为:

整个:

节点:

变了。

最终:


Destroy

↓

Create

全部:

重新。

创建。


Key 为什么重要?

真正:

有:

Key:

例如:


ForEach(

list,

item=>{

Text(item.name)

},

item=>item.id

)

Runtime:

真正:

比较:

不是:

Index。

而是:


id

例如:

第一次:


1

2

3

第二次:


2

1

3

Diff:

立即:

知道:

只是:

交换:

位置。

不是:

删除。

创建。


Key Lookup

真正:

Runtime:

更接近:


unordered_map

<Key,

FrameNode*>

例如:


1001

↓

FrameNode

查找:

复杂度:


O(1)

所以:

大型:

列表。

依然:

很快。


Diff 真正流程

真正:

Runtime。

更接近:


Compare Type

↓

Compare Key

↓

Compare Property

↓

Compare Children

不是:

直接:

递归。

全部:

扫描。


第一步 Compare Type

例如:

旧:


Text

新:


Button

不用:

比较:

Property。

直接:


Destroy

↓

Create

因为:

类型。

已经:

不同。


第二步 Compare Property

例如:


Text

↓

content=A

变成:


Text

↓

content=B

真正:

Patch:

只有:


SetContent

Layout。

不用:

重新。

计算。


Compare Layout

例如:


.width(100)

变成:


.width(200)

真正:

Runtime:

标记:


Layout Dirty

不是:

Paint Dirty。

因为:

尺寸:

变化。

需要:

重新:

布局。


Paint Dirty

例如:


.backgroundColor(Color.Red)

变成:


.backgroundColor(Color.Blue)

真正:

只需要:


Paint Dirty

Layout。

完全:

不用。

重新:

Measure。


Dirty Flag

真正:

FrameNode。

里面:

维护:


enum DirtyFlag{

Layout,

Paint,

Property,

Render

}

不同:

变化。

不同:

Dirty。

例如:


Text

内容

↓

Property Dirty

例如:


Width

↓

Layout Dirty

例如:


Opacity

↓

Paint Dirty

粒度:

非常:

细。


为什么动画不卡?

例如:

Opacity:

动画:


1

↓

0

如果:

每一帧:

Build。

Layout。

性能:

非常:

差。

真正:

Runtime:

只:


Paint Dirty

Layout。

完全:

跳过。

因此:

动画:

可以:

稳定:

60FPS。


Children Diff

来看:


Column

├──Text

├──Image

└──Button

第二次:


Column

├──Text

├──Image

├──Loading

└──Button

真正:

Diff:

发现:

前两个:

一样。

Button:

移动。

Loading:

新增。

于是:

Patch:


InsertNode

不是:

全部:

重建。


为什么 LazyForEach 更快?

普通:

ForEach:

真正:

Build:


10000 Item

全部:

FrameNode。

全部:

创建。

Lazy:

真正:


Visible

↓

20 Item

只有:

屏幕:

可见:

节点。

创建。

滚动:

再:

复用。

所以:

大型:

列表。

性能。

极高。


Node Reuse(节点复用)

真正:

Runtime:

维护:

一个:


Reuse Pool

例如:

列表:

滑走:


Item1

不是:

Destroy。

而是:

进入:


Reuse Pool

新的:

Item:

直接:

修改:

数据。

继续:

使用。

真正:

避免:

GC。


Diff 完整流程

执行:


this.count++

真正:

Runtime:

完整:

链路:


Setter

↓

ObservedProperty

↓

Subscriber

↓

Mark Dirty

↓

FrameScheduler

↓

Build()

↓

New FrameNode Tree

↓

Diff()

↓

Compare Type

↓

Compare Key

↓

Compare Property

↓

Generate Patch

↓

Layout

↓

Paint

↓

RenderNode

↓

RenderService

↓

GPU

↓

Display

ArkUI Diff 为什么比 Virtual DOM 更快?

React:


JS

↓

Virtual DOM

↓

DOM Patch

↓

Browser Layout

↓

Paint

HarmonyOS:


ArkTS

↓

FrameNode

↓

Diff

↓

RenderNode

↓

RenderService

少了:


DOM

Browser Engine

JS Bridge

因此:

ArkUI。

Diff。

更加:

轻量。


真正的 Diff 核心思想

整个 ArkUI Diff,并不是简单的"两棵树比较"。

它真正做的是三件事:


① 找到可以复用的节点(Reuse)

↓

② 找到真正发生变化的属性(Property Diff)

↓

③ 生成最小 Patch(Minimal Patch)

最终:

RenderService。

只处理:

真正:

变化:

的:

RenderNode。

这也是 HarmonyOS 在大型页面、长列表、高刷新率动画下依然能够保持流畅的重要原因。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐