HarmonyOS状态管理真相:无需刷新代码的奥秘
为什么 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 在大型页面、长列表、高刷新率动画下依然能够保持流畅的重要原因。
更多推荐


所有评论(0)