云计算百科
云计算领域专业知识百科平台

FrameNode 源码级深度解析—— ArkUI 为什么一切皆 FrameNode

如果说:

@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 保存结果

这种分层设计带来了几个明显优势:

  • 职责清晰:状态、约束、算法、结果分别由不同对象管理。
  • 易于扩展:新增一个组件,只需要新增对应的 LayoutAlgorithm,不必修改整个布局框架。
  • 便于优化:缓存、增量布局、局部重排都可以在 LayoutWrapper 和 GeometryNode 层完成。
  • 支持复杂布局:Flex、WaterFlow、LazyList 等都可以实现各自独立的布局策略,而无需污染公共代码。
  • 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 将渲染与应用彻底解耦的核心设计。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » FrameNode 源码级深度解析—— ArkUI 为什么一切皆 FrameNode
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!