如果说:
@Component
是开发者看到的组件。
那么:
FrameNode
就是 ArkUI Runtime 真正看到的组件。
整个 ArkUI ACE Engine 里面,出现频率最高的类不是:
TextPattern
ButtonPattern
ColumnPattern
而是:
FrameNode
几乎所有组件。
最终都会转换成:
FrameNode
可以说:
FrameNode 是整个 ArkUI 渲染引擎的核心数据结构。
从一行代码开始
例如:
Column() {
Text("HarmonyOS")
Button("登录")
}
开发者认为:
创建了:
Column
Text
Button
实际上。
Runtime 真正创建的是:
FrameNode(Column)
↓
FrameNode(Text)
↓
FrameNode(Button)
不是:
Component
为什么不直接保存 Component?
这是很多人都会问的问题。
例如:
@Component
struct LoginPage {}
为什么 Runtime 不直接保存:
LoginPage
原因非常简单:
Component 只是:
ArkTS
↓
业务逻辑
真正负责:
布局:
绘制:
动画:
事件:
的是:
FrameNode
所以 Runtime 根本不关心:
LoginPage
它关心的是:
Column
Text
Image
List
Button
FrameNode 在整个 Runtime 的位置
真正的数据流:
ArkTS
↓
Build()
↓
FrameNode Tree
↓
Layout
↓
Paint
↓
RenderNode
↓
RenderService
↓
GPU
注意:
从 Build 开始。
后面的 Runtime。
已经完全不知道:
ArkTS。
是什么。
整个 Runtime。
操作的。
全部都是:
FrameNode
FrameNode 内部结构
真正源码(ACE Engine)远比教程复杂。
抽象之后,大致如下:
class FrameNode {
public:
int64_t nodeId;
RefPtr<Pattern> pattern;
RefPtr<EventHub> eventHub;
RefPtr<FocusHub> focusHub;
RefPtr<GestureHub> gestureHub;
RefPtr<LayoutProperty> layoutProperty;
RefPtr<PaintProperty> paintProperty;
RefPtr<GeometryNode> geometryNode;
RefPtr<RenderContext> renderContext;
std::vector<RefPtr<FrameNode>> children;
WeakPtr<FrameNode> parent;
};
看到这里你会发现:
一个 FrameNode。
其实已经包含:
整个组件生命周期需要的大部分能力。
为什么设计成组合?
很多 UI 框架。
喜欢:
class Button : View
HarmonyOS 没有。
而是:
FrameNode
+
Pattern
+
LayoutProperty
+
PaintProperty
+
EventHub
为什么?
因为:
职责分离。
例如:
事件:
EventHub
布局:
LayoutProperty
绘制:
PaintProperty
FrameNode:
只是:
管理者。
不是:
执行者。
NodeId
每一个:
FrameNode。
都有:
唯一:
nodeId
例如:
1001
1002
1003
NodeId。
整个:
Runtime。
唯一。
主要作用:
查找
动画
事件
Diff
Parent 与 Children
真正:
FrameNode。
就是:
树。
例如:
Column
├──Text
├──Image
└──Button
Runtime:
真正:
就是:
children.push_back(…)
形成:
树。
不是:
数组。
不是:
链表。
为什么必须保存 Parent?
例如:
Button。
需要:
知道:
自己的:
父布局。
例如:
Column
因为:
Layout。
真正:
需要:
Parent Constraint
例如:
父组件:
宽:
300
Button:
设置:
width(500)
真正:
Layout。
最后:
只能:
300
所以:
Layout。
必须:
能够:
向上:
找到:
Parent。
Pattern 到底是什么?
很多开发者:
一直认为:
FrameNode。
负责:
按钮。
逻辑。
其实:
不是。
真正:
Button:
行为:
全部:
在:
ButtonPattern
例如:
点击:
OnClick
焦点:
Focus
Hover:
Hover
Pressed:
Pressed
全部:
Pattern。
负责。
FrameNode。
不知道:
什么:
叫:
Button。
为什么 Pattern 可以复用?
真正:
FrameNode:
只是:
容器。
Pattern:
才:
决定:
行为。
例如:
TextPattern
ButtonPattern
ListPattern
SwiperPattern
ScrollPattern
FrameNode。
可以:
挂:
任何:
Pattern。
这也是:
ArkUI。
大量:
组件。
共用:
FrameNode。
原因。
EventHub
真正:
事件。
不会:
放:
FrameNode。
例如:
.onClick()
.onTouch()
.onHover()
真正:
保存:
EventHub
内部:
更接近:
class EventHub {
ClickEvent
TouchEvent
HoverEvent
KeyEvent
}
真正:
点击:
流程:
Touch
↓
HitTest
↓
EventHub
↓
ClickCallback
GestureHub
很多人。
不知道。
Gesture。
其实:
独立。
例如:
TapGesture
LongPressGesture
PanGesture
PinchGesture
真正:
保存:
GestureHub
不是:
EventHub。
因为:
Gesture。
需要:
状态机。
例如:
DOWN
↓
MOVE
↓
UP
普通:
Click。
没有。
这么:
复杂。
FocusHub
例如:
TV。
PC。
HarmonyOS。
支持:
焦点。
例如:
↑
↓
←
→
真正:
Focus。
全部:
集中:
管理:
FocusHub
真正:
维护:
Current Focus
Next Focus
Tab Order
所以:
FrameNode。
不用:
自己:
实现。
焦点。
LayoutProperty
来看:
.width(200)
.height(100)
.margin(10)
真正:
保存:
LayoutProperty
里面:
更接近:
width
height
margin
padding
constraint
注意:
这里只是:
属性。
没有:
真正:
计算。
GeometryNode
真正:
Layout。
完成。
以后。
生成:
GeometryNode
例如:
x=20
y=50
width=180
height=100
真正:
Render。
阶段。
全部:
读取:
GeometryNode。
不是:
LayoutProperty。
为什么?
因为:
真正:
尺寸。
可能:
已经:
发生:
变化。
PaintProperty
例如:
.backgroundColor()
.borderRadius()
.opacity()
.shadow()
真正:
保存:
PaintProperty
Paint。
阶段。
直接:
读取。
Layout。
完全:
不用:
关心。
RenderContext
这是:
很多教程:
几乎:
不会:
讲:
的:
对象。
真正:
FrameNode。
最后:
真正:
连接:
RenderService。
就是:
RenderContext
它里面:
通常维护:
Transform
Opacity
Clip
Matrix
Animation
RenderLayer
RenderContext。
真正:
生成:
RenderNode。
然后:
发送:
给:
RenderService。
FrameNode 生命周期
一个 FrameNode 从创建到销毁,大致会经历下面几个阶段:
Create
↓
Attach Pattern
↓
Attach EventHub
↓
Attach LayoutProperty
↓
Build Finish
↓
Measure
↓
Layout
↓
Paint
↓
Render
↓
Detach
↓
Destroy
需要注意的是:
FrameNode 的生命周期并不一定与 Component 完全一致。
例如:
在列表复用(Reuse)场景下:
Item 滑出屏幕
↓
进入 Reuse Pool
↓
修改绑定数据
↓
重新 Attach
↓
继续使用
整个过程中:
FrameNode。
没有:
Destroy。
只是:
重新:
绑定。
这也是:
LazyForEach。
性能:
极高。
的重要:
原因。
FrameNode 为什么能支撑整个 ArkUI?
因为:
它不是:
某一个:
组件。
而是:
整个:
UI。
最小:
运行:
单元。
真正:
ArkUI Runtime。
几乎:
所有:
系统。
最终:
都:
围绕:
FrameNode。
展开:
State
↓
FrameNode
↓
Layout
↓
Paint
↓
Event
↓
Animation
↓
Accessibility
↓
RenderService
所以:
理解:
FrameNode。
基本:
就理解了:
ArkUI。
整个:
运行:
机制。
Pattern 源码级深度解析(一)—— 为什么 ArkUI 不用继承,而选择 Pattern?
上一篇我们已经知道:
整个 ArkUI Runtime 的核心对象是:
FrameNode
但是:
真正决定:
Button 为什么能点击?
Text 为什么能显示文字?
List 为什么能滚动?
Swiper 为什么能滑动?
并不是:
FrameNode
而是:
Pattern
可以说:
FrameNode 是身体。Pattern 才是灵魂。
一个 Button 到底是什么?
开发者写:
Button("登录")
很多人认为:
Runtime 创建:
Button
实际上:
真正创建的是:
FrameNode
│
▼
ButtonPattern
FrameNode 负责:
生命周期
树结构
属性管理
ButtonPattern 负责:
点击
Hover
按压
焦点
动画
状态切换
为什么不用继承?
很多 UI 框架:
都是:
View
│
├──TextView
├──ButtonView
├──ImageView
└──ListView
HarmonyOS 没有。
真正:
更接近:
FrameNode
+
Pattern
为什么?
因为:
继承越来越深。
例如:
View
↓
ScrollableView
↓
ListView
↓
WaterFlowView
↓
GridView
维护:
越来越困难。
ArkUI。
采用:
组合。
而不是:
继承。
Runtime 创建流程
例如:
Button("登录")
真正:
Runtime。
流程:
ButtonModel
↓
PatternFactory
↓
Create ButtonPattern
↓
Create FrameNode
↓
Attach Pattern
↓
Return FrameNode
注意:
真正:
创建:
Pattern。
发生在:
FrameNode。
之前。
PatternFactory
ACE Engine。
里面。
真正:
存在:
类似:
PatternFactory
例如:
switch(type){
TEXT
↓
new TextPattern()
BUTTON
↓
new ButtonPattern()
IMAGE
↓
new ImagePattern()
LIST
↓
new ListPattern()
}
所以:
FrameNode。
本身:
不知道:
自己:
是什么:
组件。
它只知道:
我有一个:
Pattern
Pattern 内部结构
真正:
Pattern。
远比:
教程:
复杂。
抽象以后:
class Pattern{
public:
virtual OnAttach();
virtual OnDetach();
virtual OnModifyDone();
virtual Measure();
virtual Layout();
virtual Paint();
virtual OnDirtyLayout();
virtual OnDirtyPaint();
}
几乎:
每一个:
生命周期。
都有:
Hook。
OnAttach
真正:
FrameNode。
第一次:
创建。
调用:
Create FrameNode
↓
Attach Pattern
↓
OnAttach()
例如:
Button。
这里:
初始化:
Gesture
Theme
Animation
Click
不是:
Constructor。
里面。
为什么不用构造函数?
因为:
Constructor。
时候:
FrameNode。
还没:
准备。
完成。
例如:
ButtonPattern()
里面。
拿不到:
FrameNode
Context
Theme
真正:
初始化。
必须:
等:
OnAttach()
OnModifyDone 是什么?
这是:
很多人:
第一次:
看:
ACE Engine。
都会:
疑惑。
例如:
开发者:
Button("登录")
.width(200)
.backgroundColor(Color.Red)
真正:
属性:
更新:
以后:
调用:
OnModifyDone()
什么意思?
就是:
所有:
Property。
已经:
修改。
完成。
现在。
Pattern。
可以:
统一:
处理。
为什么需要 OnModifyDone?
例如:
Button:
宽:
改变:
100
↓
200
颜色:
改变:
Blue
↓
Red
如果:
每一次:
Property。
修改:
都:
重新:
初始化。
性能。
极差。
真正:
Runtime。
采用:
Collect Property
↓
全部完成
↓
OnModifyDone()
一次:
处理。
ButtonPattern 做什么?
真正:
OnModifyDone。
里面:
可能:
执行:
Update Theme
↓
Update Background
↓
Update Radius
↓
Update ClickArea
↓
Mark Paint Dirty
所以:
开发者:
修改:
一个:
Property。
不会:
立即:
刷新。
Measure
真正:
Layout。
开始:
调用:
FrameScheduler
↓
Measure()
ButtonPattern。
真正:
计算:
例如:
Text Width
Padding
Border
Icon
Content
最后:
得到:
Desired Size
Layout
很多人。
认为:
Measure。
就:
布局。
其实:
不是。
真正:
Measure:
负责:
我要:
多大?
Layout:
负责:
我:
放:
哪里?
例如:
Button:
最终:
得到:
x
y
width
height
Paint
真正:
Paint。
阶段:
ButtonPattern。
不会:
自己:
画。
而是:
生成:
PaintMethod
真正:
绘制。
交给:
RenderContext
↓
RenderNode
↓
RenderService
Pattern。
只是:
描述:
怎么:
画。
TextPattern
Text。
真正:
Pattern。
负责:
字体
字体大小
换行
省略号
Baseline
RichText
Emoji
Bidi
RTL
所以:
TextPattern。
其实:
比:
ButtonPattern。
复杂。
很多。
ImagePattern
真正:
负责:
ImageSource
PixelMap
Decode
Cache
Resize
FitMode
Repeat
Animation Image
很多:
图片。
性能。
优化。
其实:
都:
在:
ImagePattern。
里面。
ScrollPattern
真正:
Scroll。
几乎:
全部:
逻辑:
都:
这里。
例如:
Velocity
Fling
EdgeEffect
Spring
Overscroll
ScrollBar
FrameNode。
完全:
不知道:
什么:
叫:
滚动。
ListPattern
真正:
复杂:
程度:
远超:
Button。
例如:
负责:
Reuse
Recycle
Cache
Sticky
Lazy
Visible
Index
EstimateHeight
也就是说:
真正:
列表。
性能。
全部:
集中:
在:
ListPattern
Pattern 为什么没有保存属性?
例如:
.width(200)
很多人。
觉得:
ButtonPattern。
保存:
200。
实际上:
不是。
真正:
保存:
在:
LayoutProperty
Pattern。
真正:
读取:
GetLayoutProperty()
这样:
所有:
组件。
统一:
管理:
Property。
Pattern 如何访问 FrameNode?
真正:
Pattern。
内部:
维护:
WeakPtr<FrameNode>
为什么:
WeakPtr?
因为:
FrameNode。
拥有:
Pattern。
如果:
Pattern。
再:
强引用:
FrameNode。
就会:
FrameNode
↓
Pattern
↓
FrameNode
循环:
引用。
GC。
无法:
释放。
所以:
ACE Engine。
这里:
全部:
采用:
WeakPtr
Pattern 生命周期
完整:
流程:
Create
↓
Attach
↓
OnAttach()
↓
Property Update
↓
OnModifyDone()
↓
Measure()
↓
Layout()
↓
Paint()
↓
Detach()
↓
Destroy()
其中:
OnModifyDone()
是:
ArkUI。
区别于:
很多:
UI。
框架。
最重要:
Hook。
为什么 Pattern 是 ArkUI 最大创新?
传统:
Android:
Button
负责:
布局
绘制
事件
动画
一个:
类。
几千:
行。
ArkUI:
真正:
拆成:
FrameNode
↓
Pattern
↓
LayoutProperty
↓
PaintProperty
↓
GeometryNode
↓
EventHub
↓
RenderContext
每一个:
对象。
职责:
非常:
单一。
这就是:
ACE Engine。
为什么:
源码。
非常:
容易:
维护。
Pattern 在整个 Runtime 中的位置
整个 ArkUI Runtime 可以抽象成:
Build()
↓
FrameNode
↓
Pattern
├──Measure()
├──Layout()
├──Paint()
├──Event
└──Animation
↓
RenderContext
↓
RenderNode
↓
RenderService
↓
GPU
可以看到:
FrameNode 管理生命周期,Pattern 实现组件行为,两者共同组成了 ArkUI 的核心运行单元。
Layout 源码级深度解析(一)—— ArkUI 为什么把 Measure 和 Layout 分开?
前面,我们已经分析了:
State
↓
Build()
↓
FrameNode
↓
Pattern
很多开发者认为:
接下来:
就是:
Render
实际上。
不是。
真正:
ArkUI。
真正:
执行:
Build
↓
Measure
↓
Layout
↓
Paint
↓
Render
很多人都会问:
为什么:
要:
分成:
Measure
+
Layout
两个:
阶段?
Android。
这样。
Flutter。
这样。
Compose。
这样。
HarmonyOS。
也是:
这样。
真正。
原因。
只有:
一个。
不知道子组件有多大,就不知道自己应该有多大;不知道自己最终有多大,也就无法决定子组件应该放在哪里。
因此。
布局必须拆成两个完全不同的阶段。
一个最简单例子
例如:
Column() {
Text("HarmonyOS")
Button("登录")
}
很多人。
认为。
Runtime。
执行:
Text
↓
Button
↓
结束
实际上。
真正:
执行:
Measure
↓
Layout
完全:
两次。
遍历。
什么是 Measure?
一句话:
Measure
↓
我要多大?
例如:
Text("HarmonyOS")
真正:
Measure。
执行:
字体大小
↓
文字宽度
↓
Padding
↓
Border
↓
Desired Size
最终:
得到:
width = 92
height = 28
这里只是:
希望的大小(Desired Size)。
不是:
最终:
大小。
什么是 Layout?
Layout。
一句话:
我放哪里?
例如:
父组件:
宽:
300
Text:
Measure:
得到:
92
Layout。
真正:
计算:
x = 104
y = 20
因此。
Measure。
解决:
尺寸。
Layout。
解决:
位置。
为什么不能一次完成?
来看:
Row() {
Button()
Button()
}
假设:
父组件:
宽:
400
如果:
不知道:
两个:
Button:
宽:
多少。
怎么:
决定:
位置?
所以:
必须:
先:
Measure:
得到:
120
100
然后:
Layout:
得到:
Button1
x=0
Button2
x=120
Constraint(约束)系统
真正:
ArkUI。
布局。
不是:
直接:
计算。
而是:
依赖:
LayoutConstraint
几乎:
所有:
组件。
Measure。
都会:
收到:
LayoutConstraint {
minWidth
maxWidth
minHeight
maxHeight
}
例如:
父组件:
告诉:
子组件:
最大:
300
子组件。
永远:
不能:
Measure:
500
Constraint 是如何传递的?
例如:
Column() {
Button()
}
真正:
流程:
Window
↓
Column
↓
Button
每一级。
都会:
向下:
传递:
Constraint。
例如:
Window:
maxWidth = 1080
Column:
收到:
后。
继续:
传给:
Button。
Button。
不能:
突破:
这个:
限制。
为什么 Constraint 很重要?
来看:
Button()
.width(99999)
很多新人:
认为:
按钮:
宽:
99999。
实际上:
真正:
Measure:
得到:
99999
Constraint:
立即:
修正:
1080
最终:
Geometry:
只有:
1080
所以:
Property。
不是:
最终:
结果。
Constraint。
才:
决定:
最终:
尺寸。
Measure 的真正流程
真正:
ACE Engine。
Measure。
大致:
如下:
Receive Constraint
↓
Measure Children
↓
Calculate Self
↓
Return Size
注意:
真正:
Measure。
不是:
只:
计算:
自己。
很多:
组件。
必须:
先:
Measure:
子组件。
Column Measure
例如:
Column(){
Text()
Button()
Image()
}
真正:
Measure:
Measure Text
↓
Measure Button
↓
Measure Image
↓
Height Sum
↓
Width Max
↓
Return
最终:
Column:
高度:
等于:
所有:
孩子:
高度:
之和。
Row Measure
真正:
算法:
完全:
相反。
Measure Text
↓
Measure Button
↓
Measure Image
↓
Width Sum
↓
Height Max
所以:
Row。
宽:
来自:
累加。
高:
来自:
最大。
Stack Measure
例如:
Stack(){
Image()
Text()
}
真正:
Measure:
Measure Image
↓
Measure Text
↓
Width Max
↓
Height Max
因为:
Stack。
不是:
排列。
而是:
覆盖。
GeometryNode 什么时候生成?
很多人:
认为:
Measure。
结束。
Geometry。
就:
有了。
实际上:
不是。
真正:
Geometry。
完整:
生成:
发生:
在:
Layout
例如:
Measure
↓
width=200
真正:
Layout:
才:
得到:
x
y
width
height
GeometryNode:
真正:
完整。
为什么 Measure 可以缓存?
例如:
按钮:
内容:
没有:
变化。
Constraint:
也:
没有:
变化。
真正:
Runtime。
直接:
Measure Cache
跳过:
整个:
Measure。
因此:
很多:
页面:
第二次:
打开。
布局:
极快。
Dirty Layout
例如:
修改:
.width(300)
真正:
FrameNode。
标记:
Layout Dirty
下一帧:
真正:
执行:
Measure
↓
Layout
如果:
修改:
.backgroundColor()
真正:
不会:
Measure。
因为:
颜色。
不会:
影响:
尺寸。
Layout 真正流程
真正:
Layout。
收到:
Geometry
+
Parent Position
然后:
计算:
Child Position
例如:
Column:
真正:
算法:
CurrentY=0
↓
Text
↓
Y+=Text.Height
↓
Button
↓
Y+=Button.Height
↓
Image
最终:
得到:
所有:
孩子:
坐标。
Flex 为什么复杂?
真正:
Flex。
不是:
简单:
排列。
真正:
需要:
计算:
Grow
Shrink
Weight
Basis
Align
Justify
因此:
FlexPattern。
源码。
远远:
超过:
普通:
Column。
Measure 为什么从下往上?
真正:
Measure:
顺序:
Text
↓
Button
↓
Column
因为:
父组件。
必须:
知道:
孩子:
尺寸。
所以:
Measure:
实际上:
更接近:
后序遍历(Post-order Traversal)。
Layout 为什么从上往下?
真正:
Layout:
顺序:
Window
↓
Column
↓
Button
因为:
孩子:
必须:
知道:
父组件:
位置。
所以:
Layout:
更接近:
前序遍历(Pre-order Traversal)。
Layout Pipeline
整个:
ArkUI。
布局。
真正:
流程:
State Changed
↓
Build()
↓
FrameNode Tree
↓
Measure()
↓
Desired Size
↓
Layout()
↓
GeometryNode
↓
Paint()
↓
RenderNode
↓
RenderService
为什么 Measure/Layout 分离能提高性能?
如果:
Measure。
和:
Layout。
混在:
一起。
那么:
任何:
位置:
变化。
都:
可能:
重新:
计算:
尺寸。
现在:
拆开:
以后:
例如:
Scroll
↓
Y 改变
真正:
很多:
组件。
只需要:
Layout
不用:
Measure
又比如:
Opacity
↓
变化
甚至:
Layout。
都:
不用。
直接:
Paint。
因此:
ArkUI。
采用:
三级:
Dirty:
Measure Dirty
↓
Layout Dirty
↓
Paint Dirty
最大程度减少了不必要的计算。
Layout 子系统总架构
整个 Layout Engine 可以抽象成下面这张图:
FrameNode
│
▼
Pattern::Measure()
│
▼
Desired Size
│
▼
Pattern::Layout()
│
▼
GeometryNode
│
▼
Paint()
│
▼
RenderContext
│
▼
RenderService
这里可以看到:
Layout Engine 并不是计算像素,而是在不断地传递约束(Constraint)、计算期望尺寸(Desired Size)以及生成最终几何信息(GeometryNode)。
这也是 ArkUI、Flutter、Jetpack Compose 等现代声明式 UI 框架在布局设计上的共同思想。
Constraint(约束系统)源码级深度解析(一)—— ArkUI 布局引擎真正的核心
前面我们已经知道:
整个 ArkUI Layout Engine 的核心流程是:
Build()
↓
Measure()
↓
Layout()
↓
Paint()
但是:
真正决定整个布局结果的。
其实不是:
Measure
也不是:
Layout
而是:
LayoutConstraint
可以说:
整个 ArkUI 的布局,本质上就是 Constraint(约束)的不断传递、计算和收敛。
为什么需要 Constraint?
来看一个例子。
Column() {
Button("登录")
}
.width(300)
Button:
没有:
指定:
宽度。
那么:
按钮。
到底:
应该:
多宽?
很多新人:
觉得:
Button。
自己:
决定。
实际上:
不是。
真正:
决定:
Button。
宽度:
的是:
Parent Constraint
也就是:
父组件。
给它的:
约束。
Runtime 真正流程
真正:
布局。
开始:
第一步:
不是:
Measure。
而是:
生成:
LayoutConstraint
例如:
Window:
宽:
1080
真正:
Runtime:
创建:
LayoutConstraint {
minWidth = 0
maxWidth = 1080
minHeight = 0
maxHeight = 2400
}
然后:
一路:
向下:
传递。
LayoutConstraint 数据结构
ACE Engine。
真正:
比:
教程。
复杂。
抽象:
以后:
更接近:
class LayoutConstraint {
SizeF minSize;
SizeF maxSize;
OptionalSizeF selfIdealSize;
OptionalSizeF parentIdealSize;
float percentReferenceWidth;
float percentReferenceHeight;
LayoutDirection direction;
}
注意。
这里:
不仅仅:
只有:
min
max
还有:
IdealSize
这也是:
ArkUI。
和:
传统:
View。
最大的:
区别。
什么叫 IdealSize?
很多人。
第一次。
看到:
源码。
都会:
疑惑。
例如:
Button:
真正:
Measure:
得到:
120
Constraint:
允许:
300
那么:
真正:
Button:
希望:
自己:
是:
120
这就是:
Ideal Size
最终:
Layout。
是否:
采用。
还要:
继续:
计算。
为什么不是固定宽度?
例如:
开发者:
写:
.width(200)
很多人:
觉得:
最终:
一定:
200。
其实。
不是。
例如:
父组件:
只有:
150
真正:
Runtime:
执行:
Ideal
↓
Constraint
↓
Clamp
最后:
得到:
150
不是:
200。
Clamp(约束收敛)
真正:
ACE Engine。
大量:
使用:
Clamp()
↓
min
↓
ideal
↓
max
例如:
Ideal = 500
Max = 300
最终:
300
例如:
Ideal = 10
Min = 50
最终:
50
因此。
真正:
尺寸:
永远:
不会:
突破:
Constraint。
Constraint 为什么一直向下传?
来看:
Column() {
Row() {
Button()
}
}
真正:
Constraint:
传递:
Window
↓
Column
↓
Row
↓
Button
每一级:
都:
可能:
修改:
Constraint。
例如:
Column:
减去:
Padding
Margin
然后:
继续:
向下:
传递。
Padding 如何影响 Constraint?
例如:
Column()
.padding(20)
父:
Constraint:
300
真正:
子组件:
收到:
不是:
300。
而是:
260
因为:
左右:
Padding:
各:
20。
真正:
计算:
300
–
20
–
20
=
260
所以:
Padding。
本质:
不是:
移动:
组件。
而是:
修改:
Constraint。
Margin 为什么不同?
很多新人:
容易:
混。
真正:
Margin:
不会:
影响:
孩子:
Constraint。
它:
影响:
的是:
Parent Layout
例如:
Button()
.margin(20)
Button:
Measure:
宽:
仍然:
100。
真正:
Layout:
位置:
变成:
x = 20
因此:
Padding。
修改:
Constraint。
Margin。
修改:
Position。
百分比宽度
例如:
.width("50%")
真正:
Runtime:
不是:
立即:
计算。
而是:
保存:
PercentReference
真正:
Measure:
阶段:
读取:
Parent:
Constraint。
例如:
Parent = 600
真正:
计算:
600 × 50%
↓
300
所以:
百分比。
必须:
Measure。
才能:
得到。
为什么 Constraint 能支持折叠屏?
来看:
手机:
Width = 1080
折叠:
以后:
Width = 1800
真正:
Window:
Constraint:
立即:
变成:
MaxWidth = 1800
下面:
所有:
组件。
重新:
Measure。
Layout。
整个:
UI。
自动:
适配。
开发者:
根本:
不用:
修改:
Button。
代码。
Constraint 与 SafeArea
例如:
刘海屏:
真正:
Window:
宽:
1080
但是:
SafeArea:
左右:
各:
40。
真正:
传递:
Constraint:
变成:
1000
所以:
Button。
永远:
不会:
跑:
到:
刘海。
里面。
Constraint 与 Scroll
例如:
Scroll:
真正:
收到:
Constraint:
Height = 800
但是:
孩子:
真正:
Measure:
得到:
3000
为什么:
没有:
报错?
因为:
ScrollPattern。
真正:
修改:
Constraint:
允许:
Infinite Height
真正:
更接近:
maxHeight = Infinity
所以:
List。
才能:
无限:
增长。
Infinity Constraint
很多:
组件。
都会:
收到:
∞
例如:
Column
List
Scroll
WaterFlow
为什么?
因为:
它们:
需要:
知道:
孩子:
真正:
大小。
如果:
限制:
800。
永远:
无法:
计算:
滚动:
距离。
Constraint 为什么支持响应式?
真正:
ArkUI:
不是:
判断:
设备。
而是:
判断:
Constraint。
例如:
MaxWidth
<600
↓
Phone
600~840
↓
Tablet
>840
↓
Desktop
所以:
真正:
响应式:
布局。
完全:
依赖:
Constraint。
不是:
DeviceType。
Constraint Cache
真正:
ACE Engine。
还有:
一个:
优化。
例如:
Constraint:
没有:
变化。
真正:
FrameNode:
保存:
Last Constraint
下一帧:
收到:
一样:
Constraint。
直接:
Skip Measure
因此:
页面:
滚动。
不会:
疯狂:
重新:
Measure。
Constraint 更新流程
例如:
窗口:
缩放:
真正:
Runtime:
执行:
Window Resize
↓
New Constraint
↓
Mark Layout Dirty
↓
Measure()
↓
Layout()
↓
GeometryNode Update
↓
Paint()
↓
RenderService
这里只会影响:
真正:
受到:
Constraint。
变化:
影响:
的:
节点。
不是:
整个:
页面。
Constraint 在整个 Layout Engine 中的位置
最终:
ArkUI。
布局。
可以:
抽象:
成:
Window
↓
LayoutConstraint
↓
Pattern::Measure()
↓
Desired Size
↓
Constraint Clamp
↓
Pattern::Layout()
↓
GeometryNode
↓
Paint()
↓
RenderNode
↓
RenderService
这里可以看到:
Measure 并不是随意计算尺寸,而是在 Constraint 的约束下不断收敛,最终得到一个合法的 GeometryNode。
这也是 ArkUI 能够同时适配手机、平板、PC、折叠屏、车机等不同设备的根本原因。
源码角度补充:ConstraintSpace 与 LayoutWrapper
前面为了理解流程,我们简化了很多细节。但如果从 ACE Engine 源码来看,仅仅知道 LayoutConstraint 还不够。
真正参与布局的对象实际上还有两个核心角色:
FrameNode
│
▼
LayoutWrapper
│
▼
LayoutConstraintF
│
▼
LayoutAlgorithm
很多人阅读源码时,会发现真正调用 Measure() 的不是 FrameNode,而是 LayoutWrapper。
这是 HarmonyOS 布局引擎和很多 UI 框架最大的区别之一。
为什么又多了一个 LayoutWrapper?
如果直接在 FrameNode 上 Measure,会遇到一个问题。
例如:
FrameNode
里面保存着:
GeometryNode
LayoutProperty
RenderContext
Pattern
EventHub
如果 Layout 在计算过程中直接修改这些对象:
Measure
↓
修改 Geometry
↓
Layout 失败
UI 树就已经被污染了。
因此 ACE Engine 引入了:
LayoutWrapper
作为:
FrameNode 的一次布局快照(Layout Snapshot)
真正布局结束之后,再统一提交结果。
LayoutWrapper 内部结构(源码抽象)
大致可以抽象为:
class LayoutWrapper {
RefPtr<FrameNode> hostNode;
RefPtr<GeometryNode> geometry;
RefPtr<LayoutProperty> property;
RefPtr<LayoutAlgorithm> algorithm;
std::vector<RefPtr<LayoutWrapper>> children;
};
这里最重要的是:
GeometryNode
不是直接修改原始对象。
而是在 Wrapper 中计算。
成功以后:
Commit()
一次性写回。
这样整个 Layout Engine 更安全,也方便做中断、回滚和增量更新。
LayoutAlgorithm 源码级深度解析(一)—— ArkUI 真正的布局算法都藏在这里
上一篇我们分析了:
LayoutConstraint
很多开发者认为:
Constraint。
计算完成。
布局。
就结束了。
实际上。
真正。
ArkUI。
布局。
才刚刚开始。
真正。
ACE Engine。
执行:
FrameNode
↓
LayoutWrapper
↓
LayoutAlgorithm
↓
GeometryNode
真正:
负责:
布局计算的。
不是:
FrameNode。
不是:
Pattern。
而是:
LayoutAlgorithm
可以说:
LayoutAlgorithm 才是真正负责计算组件尺寸和坐标的对象。
为什么还要 LayoutAlgorithm?
很多人:
第一次:
看:
ACE Engine。
都会:
觉得:
奇怪。
为什么:
已经:
有:
Pattern
还:
需要:
LayoutAlgorithm
原因:
其实:
非常:
简单。
Pattern。
负责:
行为
LayoutAlgorithm。
负责:
布局
例如:
Button。
点击。
Hover。
动画。
属于:
ButtonPattern
但是:
Button。
宽:
多少?
高:
多少?
文字。
放:
哪里?
属于:
ButtonLayoutAlgorithm
真正:
实现:
职责:
再次:
拆分。
Runtime 创建流程
真正:
Build。
结束:
以后:
真正:
Runtime。
执行:
FrameNode
↓
Create LayoutWrapper
↓
Create LayoutAlgorithm
↓
Measure()
↓
Layout()
所以。
LayoutAlgorithm。
生命周期。
其实:
非常:
短。
每一次:
布局。
都会:
创建:
新的:
Algorithm。
LayoutAlgorithm 内部结构
真正:
ACE Engine。
里面。
LayoutAlgorithm。
抽象:
以后:
更接近:
class LayoutAlgorithm {
public:
virtual void Measure();
virtual void Layout();
};
几乎:
所有:
组件。
都会:
继承:
它。
例如:
TextLayoutAlgorithm
ButtonLayoutAlgorithm
RowLayoutAlgorithm
ColumnLayoutAlgorithm
FlexLayoutAlgorithm
ListLayoutAlgorithm
GridLayoutAlgorithm
WaterFlowLayoutAlgorithm
为什么不是 Pattern Measure?
很多人:
第一次:
阅读:
源码。
都会:
发现:
Pattern。
也有:
Measure()
实际上:
真正:
流程:
更接近:
Pattern
↓
Create LayoutAlgorithm
↓
LayoutAlgorithm Measure
Pattern。
负责:
创建:
Algorithm。
真正:
Measure。
发生:
Algorithm。
里面。
TextLayoutAlgorithm
来看:
Text("HarmonyOS")
真正:
Measure。
执行:
读取:
FontSize
↓
读取:
FontWeight
↓
读取:
LetterSpacing
↓
读取:
LineHeight
↓
调用:
TextEngine
↓
返回:
文字宽高
真正:
不是:
自己:
计算:
字符串。
长度。
而是:
调用:
文本:
排版:
引擎。
为什么 Text 测量这么复杂?
例如:
Hello
和:
你好
长度:
完全:
不同。
再例如:
😀😀😀
Emoji。
宽度。
又:
不同。
再例如:
مرحبا
RTL。
从:
右:
向:
左。
所以:
真正:
TextLayoutAlgorithm。
里面:
大量:
调用:
Typography Engine
不是:
简单:
length × fontSize
ButtonLayoutAlgorithm
很多人:
认为:
Button。
Measure。
就是:
Text。
实际上:
不是。
真正:
流程:
Measure Text
↓
Measure Icon
↓
Padding
↓
Border
↓
MinSize
↓
Constraint
↓
Final Size
Button。
真正:
宽:
是:
多个:
对象:
共同:
决定。
RowLayoutAlgorithm
来看:
Row() {
Text()
Button()
Image()
}
真正:
Measure:
算法:
Measure Text
↓
Measure Button
↓
Measure Image
↓
Width += Child.Width
↓
Height = Max()
真正:
Layout:
算法:
CurrentX = 0
↓
Text.X = 0
↓
CurrentX += Text.Width
↓
Button.X
↓
CurrentX += Button.Width
↓
Image.X
是不是:
和:
Flex。
非常:
像?
没错。
Row。
其实:
就是:
Flex。
的一种:
特殊:
实现。
ColumnLayoutAlgorithm
真正:
Measure:
Width
=
Max()
Height
=
Sum()
真正:
Layout:
CurrentY
↓
Text
↓
CurrentY+=Height
↓
Button
↓
CurrentY+=Height
↓
Image
和:
Row。
正好:
相反。
StackLayoutAlgorithm
例如:
Stack(){
Image()
Text()
Loading()
}
真正:
Measure:
Max Width
↓
Max Height
真正:
Layout:
全部:
x=0
y=0
如果:
开发者:
指定:
Alignment。
再:
计算:
Offset。
FlexLayoutAlgorithm
真正:
复杂:
很多。
真正:
Measure:
流程:
Measure Fixed Child
↓
Measure Flexible Child
↓
Calculate Remaining Space
↓
Grow
↓
Shrink
↓
Align
真正:
支持:
flexGrow
flexShrink
flexBasis
不是:
简单:
Row。
为什么 Flex Measure 两次?
来看:
Child1
flex=1
Child2
flex=2
真正:
第一次:
Measure:
不知道:
剩余:
空间。
所以:
先:
Measure:
固定:
组件。
然后:
计算:
剩余:
宽。
第二次:
真正:
计算:
Flex。
大小。
所以:
Flex。
真正:
Measure:
通常:
两轮。
ImageLayoutAlgorithm
真正:
流程:
读取:
ImageInfo
↓
Decode Size
↓
FitMode
↓
Constraint
↓
Final Size
例如:
.objectFit(ImageFit.Contain)
真正:
LayoutAlgorithm。
负责:
计算:
Contain。
不是:
Render。
ListLayoutAlgorithm
真正:
最复杂:
之一。
例如:
10000:
Item。
真正:
不会:
Measure:
10000。
而是:
Viewport
↓
Visible Item
↓
Measure
↓
Cache
↓
Estimate Height
真正:
屏幕:
只有:
20。
个。
真正:
Measure:
也:
只有:
20。
个。
为什么 List 能秒开?
很多人:
认为:
List。
性能。
来自:
LazyForEach。
其实:
不是。
真正:
核心:
是:
Estimate Height
例如:
10000:
Item。
真正:
第一次:
根本:
不用:
Measure:
全部。
直接:
估算:
10000
×
80
↓
800000
滚动:
以后:
再:
慢慢:
修正。
这就是:
大型:
列表。
不卡的:
核心。
WaterFlowLayoutAlgorithm
真正:
瀑布流。
不是:
Row。
也:
不是:
Grid。
真正:
维护:
Column1 Height
Column2 Height
Column3 Height
每:
新增:
Item。
都会:
找到:
当前:
最短:
列
然后:
放:
进去。
真正:
算法:
复杂度:
O(logN)
而不是:
暴力:
扫描。
LayoutAlgorithm 生命周期
真正:
一次:
布局:
流程:
Create Algorithm
↓
Measure
↓
Layout
↓
Generate Geometry
↓
Destroy Algorithm
注意:
Algorithm。
一般:
不会:
长期:
存在。
真正:
长期:
存在:
的是:
FrameNode
Pattern
Property
为什么要设计成短生命周期?
例如:
按钮:
10000
个。
如果:
每个:
LayoutAlgorithm。
一直:
保存。
内存:
非常:
浪费。
真正:
ACE Engine。
采用:
需要:
创建
↓
用完:
释放
因此:
Layout。
阶段。
内存。
占用:
非常:
低。
LayoutAlgorithm 在整个 Layout Engine 中的位置
真正:
ArkUI。
布局。
完整:
流程:
FrameNode
↓
LayoutWrapper
↓
LayoutConstraint
↓
LayoutAlgorithm
↓
Measure()
↓
Layout()
↓
GeometryNode
↓
Commit
↓
Paint
↓
RenderNode
↓
RenderService
到这里,你会发现:
真正负责布局计算的并不是 FrameNode,而是每一种组件对应的 LayoutAlgorithm。
不同组件有不同算法,但它们都遵循统一的 Measure/Layout 生命周期,因此 ArkUI 能够在保持一致框架的同时,支持 Text、Button、Flex、List、Grid、WaterFlow 等完全不同的布局行为。
ACE Engine 源码中的设计亮点
如果把整个布局子系统总结成一句话:
FrameNode 保存状态
↓
LayoutWrapper 保存布局上下文
↓
LayoutConstraint 提供约束
↓
LayoutAlgorithm 执行算法
↓
GeometryNode 保存结果
这种分层设计带来了几个明显优势:
Paint 源码级深度解析(一)—— ArkUI 为什么 Paint 不直接绘制到屏幕?
前面我们已经分析了整个 Layout Engine。
真正:
完成:
Layout。
以后。
Runtime。
得到:
GeometryNode
里面:
保存:
x
y
width
height
很多开发者:
认为。
接下来:
就是:
Canvas.drawXXX()
其实。
完全:
不是。
真正:
ACE Engine。
真正:
执行:
Layout
↓
Paint
↓
RenderNode
↓
RenderService
↓
GPU
这里:
Paint。
并不会:
直接:
绘制。
屏幕。
这是:
ArkUI。
区别于:
很多:
Canvas UI。
最大的:
地方。
Paint 到底是什么?
很多教程:
一句话:
Paint
↓
绘制
实际上。
真正:
Paint。
负责:
的是:
Generate Draw Command
也就是:
生成:
绘制:
命令。
不是:
立即:
画。
例如:
Text("HarmonyOS")
真正:
Paint。
不会:
立即:
执行:
canvas.drawText(…)
而是:
生成:
类似:
DrawText
x=20
y=50
font=16
color=#333
保存:
下来。
最后:
统一:
提交。
Runtime 真正流程
真正:
Pipeline:
如下:
Build()
↓
Measure()
↓
Layout()
↓
Paint()
↓
DrawCommand
↓
RenderNode
↓
RenderService
↓
GPU
真正:
GPU。
直到:
最后:
一步。
才:
知道:
UI。
是什么。
为什么不直接 Canvas?
例如:
Android。
传统:
View。
很多:
组件。
直接:
onDraw(Canvas canvas)
HarmonyOS。
没有。
为什么?
因为:
Canvas。
属于:
CPU。
真正:
ArkUI。
希望:
RenderService。
统一:
调度。
GPU。
因此:
Paint。
阶段。
不能:
立即:
画。
PaintMethod
真正:
ACE Engine。
每一个:
组件。
都会:
创建:
PaintMethod
例如:
TextPaintMethod
ButtonPaintMethod
ImagePaintMethod
ListPaintMethod
真正:
Paint。
执行:
Pattern
↓
Create PaintMethod
↓
Generate DrawCommand
不是:
Pattern。
自己:
画。
PaintMethod 内部结构
抽象:
以后:
更接近:
class PaintMethod {
public:
virtual GetContentDrawFunction();
virtual GetOverlayDrawFunction();
virtual GetForegroundDrawFunction();
};
很多:
开发者:
第一次:
看到:
都会:
疑惑。
为什么:
三个:
Draw?
原因:
就是:
ArkUI。
真正:
支持:
图层。
Content Layer
例如:
Button:
真正:
主体:
内容:
Background
↓
Text
↓
Icon
全部:
属于:
Content
Foreground Layer
例如:
Button:
获得:
焦点:
真正:
绘制:
Focus Ring
例如:
Hover:
真正:
绘制:
Highlight
真正:
属于:
Foreground
不是:
Content。
Overlay Layer
例如:
Tooltip:
Popup:
Selection:
真正:
绘制:
在:
Overlay
例如:
Text:
选择:
文字:
真正:
蓝色:
区域。
就是:
Overlay。
为什么分层?
来看:
Button:
Hover。
如果:
没有:
Layer。
真正:
刷新:
Button
↓
Background
↓
Text
↓
Icon
全部:
重新:
画。
现在:
只:
更新:
Foreground
性能:
极高。
PaintProperty
很多:
开发者:
认为:
PaintMethod。
保存:
颜色。
实际上:
不是。
真正:
颜色:
全部:
保存:
PaintProperty
例如:
.backgroundColor(Color.Red)
.opacity(0.5)
.shadow()
.borderRadius()
真正:
保存:
PaintProperty
PaintMethod。
只是:
读取。
GeometryNode 在 Paint 中有什么用?
真正:
Paint。
第一步:
读取:
GeometryNode
例如:
x=20
y=50
width=300
height=80
然后:
生成:
真正:
DrawCommand。
如果:
没有:
Geometry。
Paint。
根本:
不知道:
画:
哪里。
DrawContext
真正:
ACE Engine。
不会:
直接:
传:
Canvas。
而是:
DrawContext
里面:
通常:
包含:
Canvas
Geometry
PaintProperty
RenderContext
真正:
PaintMethod。
拿到:
DrawContext。
才能:
开始:
生成:
绘制:
命令。
DrawCommand
真正:
RenderService。
收到:
不是:
Button。
不是:
Text。
而是:
DrawRect
↓
DrawImage
↓
DrawText
↓
DrawPath
类似:
GPU。
命令。
所以:
RenderService。
完全:
不知道:
什么:
叫:
Button。
RenderNode
真正:
Paint。
结束:
以后。
生成:
RenderNode
里面:
保存:
Transform
Clip
Opacity
DrawCommand
Children
RenderNode。
真正:
对应:
GPU。
节点。
不是:
FrameNode。
为什么还有 RenderNode?
很多人:
第一次:
看:
源码。
都会:
疑惑。
已经:
有:
FrameNode。
为什么:
还:
需要:
RenderNode?
原因:
非常:
简单。
FrameNode。
保存:
State
Property
Event
Pattern
GPU。
根本:
不需要。
GPU。
真正:
需要:
只有:
Geometry
DrawCommand
Transform
所以:
Runtime。
真正:
生成:
RenderNode。
提交:
GPU。
Paint Dirty
例如:
修改:
.backgroundColor(Color.Blue)
真正:
FrameNode:
标记:
Paint Dirty
下一帧:
真正:
执行:
Paint()
↓
RenderNode Update
Layout。
不会:
执行。
Measure。
不会:
执行。
Opacity 为什么不卡?
例如:
动画:
.animateTo({
duration:300
}){
this.opacity=0
}
真正:
Runtime。
每一帧:
更新:
Opacity
真正:
Dirty:
只有:
Paint Dirty
真正:
Layout。
完全:
跳过。
Measure。
完全:
跳过。
所以:
Opacity。
动画。
可以:
稳定:
60FPS。
Clip 为什么不用重新 Paint?
例如:
Scroll。
真正:
只是:
Clip Rect
变化。
真正:
DrawCommand。
很多:
情况下:
不用:
重新:
生成。
真正:
更新:
的是:
RenderNode Clip
因此:
Scroll。
性能。
极高。
RenderContext
整个:
Paint。
最终:
都:
会:
更新:
RenderContext
真正:
里面:
保存:
Transform
Opacity
Scale
Rotation
Shadow
Mask
Clip
RenderContext。
真正:
负责:
同步:
RenderNode。
Paint Pipeline
真正:
完整:
流程:
FrameNode
↓
Pattern
↓
PaintMethod
↓
DrawContext
↓
DrawCommand
↓
RenderNode
↓
RenderContext
↓
RenderService
注意:
整个:
Paint。
过程。
没有:
真正:
GPU。
GPU。
真正:
开始:
工作。
是在:
RenderService。
收到:
RenderNode。
以后。
为什么 Paint 和 Render 分离?
很多:
UI:
框架。
直接:
Paint
↓
GPU
HarmonyOS。
没有。
真正:
拆成:
Paint
↓
RenderNode
↓
RenderService
↓
GPU
原因:
就是:
RenderService。
是:
独立:
进程。
ACE Engine。
和:
GPU。
并:
不在:
同一个:
进程。
这样:
应用:
崩溃。
不会:
直接:
影响:
整个:
图形:
系统。
RenderNode 树
真正:
FrameNode:
树:
Column
├──Text
├──Image
└──Button
Paint。
以后:
变成:
RenderNode
├──TextNode
├──ImageNode
└──ButtonNode
这里:
已经:
没有:
EventHub。
没有:
Pattern。
没有:
LayoutProperty。
只:
剩下:
渲染:
信息。
Paint 子系统总结
整个 Paint Engine 可以抽象成下面这张图:
GeometryNode
│
▼
PaintProperty
│
▼
PaintMethod
│
▼
DrawCommand
│
▼
RenderNode
│
▼
RenderContext
│
▼
RenderService
可以看到:
Paint 阶段并不是“画图”,而是把 UI 描述转换为一组标准化的绘制命令,并组织成 RenderNode 树,为 RenderService 做准备。
这也是 HarmonyOS 能够做到:
- UI 线程与渲染线程解耦
- 高性能动画
- 局部刷新(Dirty Region)
- GPU 批量提交(Batch Submit)
- 跨进程渲染
的核心基础。
RenderService(RS)源码级深度解析(一)—— HarmonyOS 为什么把渲染放到独立进程?
到目前为止,我们已经分析完了 ArkUI Runtime。
整个调用链已经变成:
@State
↓
ObservedProperty
↓
Build()
↓
FrameNode
↓
Layout
↓
Paint
↓
RenderNode
很多开发者认为:
下一步:
就是:
GPU
其实。
还差:
整个 HarmonyOS 图形系统最重要的一层:
RenderService(RS)
如果说:
ACE Engine。
负责:
生成 UI
那么:
RenderService。
负责:
真正让 UI 出现在屏幕上
什么是 RenderService?
一句话:
RenderService(RS)就是 HarmonyOS 的系统级渲染服务。
可以理解成:
ArkUI
↓
RenderService
↓
GPU
↓
Display
它并不是:
应用的一部分。
而是:
系统进程。
也就是说:
你的 App。
和:
RenderService。
并不在:
同一个:
进程。
为什么设计成独立进程?
这是 HarmonyOS 图形架构最大的特点之一。
很多传统 UI 框架:
例如:
Android 早期:
App
↓
Canvas
↓
GPU
全部:
发生:
在:
App。
里面。
如果:
App:
卡住:
GPU。
也:
容易:
受到:
影响。
HarmonyOS。
设计:
变成:
App
↓
ACE Engine
↓
IPC
↓
RenderService
↓
GPU
真正:
绘制:
完全:
交给:
RS。
为什么这样更稳定?
例如:
App:
发生:
死循环:
while(true){
}
UI。
线程:
已经:
卡死。
但是:
RenderService。
仍然:
运行。
系统:
还能:
继续:
绘制:
其他:
应用。
不会:
整个:
图形:
系统:
崩溃。
这就是:
系统:
级:
隔离。
RenderService 在系统中的位置
整个:
HarmonyOS。
图形:
架构:
更接近:
Application
↓
ArkUI
↓
ACE Engine
↓
RenderNode Tree
↓
IPC
↓
RenderService
↓
RSNode Tree
↓
RenderThread
↓
GPU
↓
Display
注意:
从:
RenderService。
开始。
已经:
完全:
不知道:
什么:
叫:
Button。
什么:
叫:
Text。
RenderNode 为什么不能直接给 GPU?
来看:
Paint。
生成:
RenderNode
很多人:
觉得:
已经:
可以:
GPU。
其实:
还:
不行。
原因:
就是:
RenderNode。
属于:
ACE。
对象。
GPU。
根本:
不认识。
真正:
RS。
需要:
转换:
成:
RSNode
RSNode 到底是什么?
真正:
RenderService。
维护:
的是:
RSNode Tree
例如:
RSRootNode
├──RSCanvasNode
├──RSTextNode
├──RSImageNode
└──RSSurfaceNode
可以看到:
已经:
没有:
FrameNode。
没有:
Pattern。
没有:
EventHub。
只:
剩下:
真正:
渲染:
节点。
为什么还要 RSNode?
很多开发者:
第一次:
看:
源码。
都会:
觉得:
重复:
设计。
实际上:
不是。
RenderNode:
属于:
App。
RSNode:
属于:
系统。
两个:
对象。
生命周期:
完全:
不同。
例如:
App:
退出。
RenderNode:
销毁。
RS:
收到:
IPC。
以后。
再:
删除:
RSNode。
IPC 是如何工作的?
真正:
ACE Engine。
不会:
直接:
调用:
RS。
而是:
通过:
IPC
发送:
命令。
例如:
CreateNode
↓
UpdateNode
↓
DeleteNode
↓
Draw
真正:
发送:
的是:
序列化:
数据。
不是:
对象。
一次 Text 更新到底发生了什么?
例如:
开发者:
执行:
this.name = "HarmonyOS"
真正:
完整:
流程:
State
↓
Build()
↓
Diff
↓
Paint
↓
RenderNode Update
↓
Serialize
↓
IPC
↓
RenderService
↓
RSNode Update
↓
GPU
真正:
跨:
进程。
只有:
最后:
一次:
同步。
RSNode 内部结构
真正:
源码:
远比:
教程:
复杂。
抽象:
以后:
更接近:
class RSNode {
NodeId id;
Matrix matrix;
Rect bounds;
Opacity opacity;
Clip clip;
Children children;
DrawCommand command;
}
可以:
看到:
这里:
已经:
没有:
UI。
逻辑。
全部:
都是:
GPU:
数据。
RSCanvasNode
很多:
普通:
组件。
最终:
都会:
转换:
成:
RSCanvasNode
里面:
真正:
保存:
DrawRect
DrawText
DrawImage
DrawPath
这些:
命令。
最后:
GPU:
直接:
执行。
RSSurfaceNode
真正:
复杂:
一点:
的是:
RSSurfaceNode
例如:
Video
Camera
XComponent
Web
这些:
组件。
不是:
普通:
DrawCommand。
而是:
独立:
Surface。
真正:
GPU。
直接:
合成。
所以:
视频。
播放。
不会:
经过:
Canvas。
为什么 Video 不会卡 UI?
因为:
真正:
视频:
流程:
不是:
Video
↓
Canvas
而是:
Video Decoder
↓
SurfaceBuffer
↓
RSSurfaceNode
↓
GPU Compose
UI。
和:
Video。
完全:
独立。
RSNode Tree
真正:
RenderService。
维护:
一棵:
RS Tree
例如:
Root
├──Window
│
├──Page
│
├──Column
│
├──Image
│
└──Button
最终:
GPU。
遍历:
这棵:
树。
不是:
FrameNode。
为什么 RSTree 可以跨应用?
很多:
人:
不知道。
真正:
RenderService。
里面:
不是:
只有:
你的:
App。
而是:
App A
↓
RSNode
App B
↓
RSNode
System UI
↓
RSNode
最后:
统一:
Compose。
所以:
通知栏。
悬浮窗。
输入法。
都:
可以:
一起:
显示。
Dirty Region
如果:
按钮:
颜色:
变化。
真正:
RS。
不会:
重绘:
整个:
屏幕。
真正:
生成:
Dirty Region
例如:
20
50
200
80
GPU。
真正:
只:
更新:
这一块:
区域。
为什么滚动这么流畅?
例如:
List。
滚动。
真正:
RS。
很多:
情况下:
更新:
的是:
Transform
不是:
DrawCommand。
所以:
GPU。
直接:
移动:
Layer。
不用:
重新:
Raster。
性能:
极高。
RS 与 Animation
真正:
ArkUI:
动画。
很多:
时候:
真正:
执行:
位置:
不是:
ACE。
而是:
RS。
例如:
.opacity()
.scale()
.translate()
.rotate()
真正:
动画:
参数:
发送:
RS。
以后:
GPU:
每帧:
自己:
更新。
ACE。
根本:
不用:
参与。
所以:
即使:
JS。
线程:
偶尔:
卡顿。
动画:
依然:
非常:
流畅。
RenderThread
真正:
RenderService。
内部:
还有:
专门:
线程。
例如:
IPC Thread
↓
Render Thread
↓
GPU Thread
真正:
RenderThread。
负责:
生成:
GPU。
CommandBuffer。
不是:
主线程。
RenderService 完整流程
整个:
RS。
真正:
执行:
如下:
Receive IPC
↓
Deserialize
↓
Update RSNode
↓
Dirty Region
↓
Layer Merge
↓
Render Thread
↓
GPU Command
↓
Swap Buffer
↓
Display
这里:
真正:
GPU。
开始:
工作。
为什么 HarmonyOS 的动画更稳定?
因为:
动画:
真正:
运行:
在:
RenderService。
而不是:
ArkTS。
例如:
.animateTo(){
opacity=0
}
真正:
ACE:
只:
发送:
一次:
Animation
Start
剩余:
300ms。
全部:
RS。
完成。
这就是:
为什么:
即使:
ArkTS。
偶尔:
GC。
动画:
仍然:
不会:
明显:
掉帧。
RenderService 子系统架构
整个 HarmonyOS 图形系统可以抽象为:
ArkTS
│
▼
ACE Engine
│
▼
RenderNode Tree
│
▼
IPC
│
▼
RenderService
│
▼
RSNode Tree
│
▼
Dirty Region
│
▼
RenderThread
│
▼
GPU Command Buffer
│
▼
Display
到这里,可以看出:
ACE Engine 的职责是构建 UI 和生成渲染描述,而 RenderService 的职责是维护系统级渲染树、管理图层、合成画面,并最终驱动 GPU 完成绘制。
这也是 HarmonyOS 将渲染与应用彻底解耦的核心设计。
网硕互联帮助中心






评论前必须登录!
注册