OpenHarmony JS‑Lite 应用渲染全流程详解:从 JS 到 UI 组件绘制
本文章完整讲解一条 HML/CSS/JS 页面是如何被解析、布局、绘制,最终变成液晶屏上像素的全过程。
一、整体架构总览
ACE Lite 采用分层 + 事件驱动 + 增量渲染的架构,从上到下共 7 层:
┌─────────────────────────────────────────────────────────────┐
│ ① JS 应用层 HML / CSS / JS(开发者写的页面) │
├─────────────────────────────────────────────────────────────┤
│ ② ACE Engine Lite JS 引擎(JerryScript) + 组件树(Component) │
│ 解析 HML/CSS、管理样式、响应数据变化 │
├─────────────────────────────────────────────────────────────┤
│ ③ UI Lite 原生层 UIView / UIViewGroup / UIList / RootView │
│ 每个 JS 组件对应一个 Native 视图对象 │
├─────────────────────────────────────────────────────────────┤
│ ④ 布局系统 FlexLayout / ListLayout │
│ 计算每个视图的 X/Y/Width/Height │
├─────────────────────────────────────────────────────────────┤
│ ⑤ 渲染引擎 RootView::Render(脏区/增量渲染) │
│ 软渲染 soft_engine 绘制像素 │
├─────────────────────────────────────────────────────────────┤
│ ⑥ HAL 图形引擎 GfxEngines(Blit/Fill 抽象接口) │
│ 屏蔽具体硬件差异 │
├─────────────────────────────────────────────────────────────┤
│ ⑦ 驱动层 Framebuffer / LCD 控制器(hi_fbdev) │
│ 最终把像素送到显示屏 │
└─────────────────────────────────────────────────────────────┘
核心思想:JS 只负责描述"长什么样"(声明式 UI),真正"怎么画"全部在 C++ 层完成。JS 与 Native 之间通过 Component 树 这个桥梁连接——每个 HML 标签对应一个 C++ Component 对象,每个 Component 持有一个 Native UIView。
二、第 ① 层:JS 应用层
开发者编写三类文件:
| 文件 | 职责 | 示例 |
|---|---|---|
| .hml | 页面结构(标签树) | div, text, list, list-item |
| .css | 样式描述 | height, width, flex-direction, align-items |
| .js | 数据与逻辑 | data: {...}, 事件回调, $item循环变量 |
<!-- index.hml -->
<div class="card">
<text class="line" for="{{lines}}">{{$item}}</text>
</div>
/* index.css */
.card {
flex-direction: column; /* 子组件竖排 */
/* 不写 height → 高度由内容自适应 */
}
// index.js
export default {
data: {
lines: ['第1行', '第2行']
}
};
这里的for、{{}}、data都是声明式描述,不是真正的 DOM 操作。JS 层不做任何像素计算。
三、第 ② 层:ACE Engine Lite(JS 引擎 + 组件树)
3.1 JS 引擎
ACE Lite 使用 JerryScript(轻量级 JS 引擎,专为嵌入式设计,内存占用 KB 级)。
- 解析并执行 index.js,拿到 export default对象
- 维护data响应式数据(数据变化触发 UI 更新)
- 执行事件回调(按键、旋转、触摸)
3.2 组件树构建
每个 HML 标签被解析成一个 Component 子类对象:
HML 标签 Component 类 Native 视图
───────── ───────────────── ────────────────
<div> → DivComponent → UIViewGroup
<text> → TextComponent → UILabel
<list> → ListComponent → UIList
<list-item> → ListItemComponent → UIViewGroup
<slider> → SliderComponent → UISlider
关键基类 Component:
class Component {
jerry_value_t options_; // JS 侧传入的属性对象
jerry_value_t descriptors_; // 子组件描述
UIView* nativeView_; // 对应的原生视图
AppStyleManager* styleManager_; // 样式管理器
Component* parent_; // 父组件
Component* childrenHead_; // 子组件链表头
};
3.3 样式解析:AppStyleManager
CSS 被解析后由 AppStyleManager统一管理,关键职责是把 CSS 字符串属性值解析成结构体。
以"高度"为例,height: 100px 和"不写 height"会被解析成不同的类型:
// 高度类型枚举(简化)
enum HeightType {
TYPE_UNKNOWN = 0, // CSS 未设置 height(自适应)
TYPE_PX, // height: 100px(固定)
TYPE_PERCENT // height: 100%(固定,相对父容器)
};
TYPE_UNKNOWN就是"自适应高度"的信号,后续布局层会据此决定是否重新计算容器高度。
四、第 ③ 层:UI Lite 原生组件层
每个 Component 在BuildViewTree时创建一个 Native 视图对象,挂载成树。
4.1 UIView 基类
class UIView {
Rect rect_; // 位置+尺寸(x, y, w, h)—— 核心!
UIView* parent_; // 父视图
UIView* nextSibling_; // 兄弟链表
bool isVisible_; // 是否可见
// 样式相关
bool isHeightSet_; // 高度是否已被布局计算过
bool isExplicitHeight_; // 高度是否是 CSS 显式指定的
};
rect_ 就是最终决定像素位置的坐标。整个渲染链路的本质,就是不断地计算和更新这些 rect_。
4.2 UIViewGroup(容器)
能包含子视图的容器(div、list-item 都是它):
class UIViewGroup : public UIView {
UIView* childrenHead_; // 子视图链表
UIView* childrenTail_;
};
4.3 关键方法
| 方法 | 作用 |
|---|---|
| LayoutChildren() | 布局子视图(排布它们的 X/Y) |
| Measure() | 测量尺寸 |
| Invalidate() | 标记需要重绘(加入脏区) |
| OnDraw() / OnPostDraw() | 绘制回调 |
一个重要认知:UIViewGroup::LayoutChildren() 只排布子视图的位置,不会重新计算容器自身的高度。容器高度是由父容器在布局时通过 SetHeight()`设置的。这也是为什么运行时增删子组件后,需要手动重新计算并 SetHeight()。
五、第 ④ 层:布局系统
布局系统负责把"声明式描述"转成"具体坐标"。
5.1 FlexLayout
ACE Lite 使用 Flex 弹性布局(对标 CSS Flexbox)。核心是 FlexLayout::LayoutChildren():
void FlexLayout::LayoutChildren()
{
// 关键判断:这个容器是否启用"自适应高度"
bool isAutoHeight = autoSize_ && !IsExplicitHeight();
// 1. 遍历所有子视图,排布它们的 X/Y
for (每个可见子视图 child) {
// 计算 child 的 top / bottom / left / right
// 设置 child->SetPosition(...)
}
// 2. 如果启用自适应高度,根据子视图最大底部位置设置容器高度
if (isAutoHeight) {
int16_t maxHeight = 0;
for (每个可见子视图 child) {
int16_t h = child->GetHeight() + marginTop + marginBottom;
maxHeight = MATH_MAX(maxHeight, h);
}
SetHeight(maxHeight); // ← 容器高度 = 子组件最大高度
}
}
下面是固定高度与自适应高度的逻辑:
- IsExplicitHeight() == false(CSS 没写 height)→ isAutoHeight == true → 重新计算高度
- IsExplicitHeight() == true(CSS 写了 height:100px)→ 固定高度,不重算
5.2 ListLayout(列表专用)
UIList 使用 ListLayout,负责:
- 计算每个 list-item 的 X/Y(item 沿主轴排列)
- 维护滚动偏移
- 管理 item 的复用(回收 + 重建,类似 RecyclerView)
5.3 Measure 与 Layout 的区别
| 阶段 | 做什么 | 何时调用 |
|---|---|---|
| Measure() | 计算尺寸(宽高) | 布局前、渲染前 |
| LayoutChildren() | 排布子视图位置 | 组件树构建后、内容变化后 |
六、第 ⑤ 层:渲染引擎(RootView)
6.1 渲染入口
渲染的"总指挥"是 WindowImpl::Render():
void WindowImpl::Render()
{
UpdateHalDisplayBuffer(); // ① 从 Surface 拿到 framebuffer 虚拟地址
if (rootView_ != nullptr) {
rootView_->Measure(); // ② 先测量
rootView_->Render(); // ③ 再渲染(绘制到 framebuffer)
}
}
void WindowImpl::Flush()
{
ISurface* surface = iWindow_->GetSurface();
if (surface != nullptr) {
surface->Unlock(); // ④ 提交缓冲,让驱动把像素刷到屏幕
}
}
完整流程:Lock 拿缓冲 → Measure → Render(画像素)→ Unlock 提交。
6.2 增量渲染(脏区机制)
ACE Lite 不是每帧全屏重绘,而是采用脏区(Invalidate Rect)增量渲染——只有发生变化的区域才重绘,大幅降低嵌入式 CPU 负担。
RootView维护两个脏区队列:
List<Rect> invalidateRects_; // 当前帧要画的脏区
List<Rect> nextInvalidateRects_; // 下一帧的脏区(渲染中新增时延后)
关键方法:
// 标记一个区域需要重绘
void RootView::AddInvalidateRect(Rect& rect, UIView* view, bool deferWhenRendering)
{
// 如果正在渲染中且 deferWhenRendering==true,则延后到下一帧
List<Rect>* target = (deferWhenRendering && isRendering_)
? &nextInvalidateRects_ : &invalidateRects_;
OptimizeAddRect(*target, commonRect); // 合并重叠脏区
}
任何视图调用 Invalidate() 后,最终都会走到这里,把它的 rect_加入脏区队列。渲染时只绘制这些脏区内的像素。
6.3 渲染遍历
RootView::Render() 遍历视图树,对每个在脏区内的视图调用OnDraw():
RootView::Render()
└─ 遍历视图树(深度优先)
└─ 对每个与脏区相交的视图
└─ curView->OnDraw(drawRect) ← 绘制自己
└─ 递归子视图 OnPostDraw() ← 绘制子视图
6.4 软渲染引擎
绘制最终落到 soft_engine(软件渲染),把矢量/文字/图片转成 ARGB8888 像素写入 framebuffer:
UIView::OnDraw()
└─ DrawUtils / DrawRect / DrawArc / DrawImage ...
└─ 计算每个像素的 ARGB 值
└─ 写入 BufferInfo.virAddr 指向的显存
七、第 ⑥ 层:HAL 图形引擎(GfxEngines)
HAL 层抽象了"如何把一块内存像素送到屏幕"这个硬件操作,核心类 GfxEngines:
bool GfxEngines::InitDriver()
{
// GfxInitialize 是厂商实现的 HAL 入口
if (GfxInitialize(&gfxFuncs_) == DISPLAY_SUCCESS) {
gfxFuncs_->InitGfx(); // 初始化显示设备
}
}
bool GfxEngines::GfxBlit(...)
{
// gfxFuncs_->Blit 是厂商提供的位块传输函数
gfxFuncs_->Blit(&srcSurface, &srcIRect, &dstSurface, &dstIRect, NULL);
}
gfxFuncs_是一组函数指针,由厂商适配层填充。这样框架代码不用关心底层是 LCD 还是 HDMI,只要调Blit即可。
八、第 ⑦ 层:驱动层(Framebuffer / LCD)
最底层是 framebuffer 设备驱动(如 hi_fbdev.cpp),负责:
- 分配显存:通过 Surface->Lock()拿到一块物理连续内存的虚拟地址(framebuffer)
- 像素写入:上层把 ARGB 值写进这块内存
- 提交刷新:Surface->Unlock()通知 LCD 控制器把显存内容刷到屏幕
CPU 写像素 → 显存(Framebuffer) → LCD 控制器(DMA) → 液晶面板显示
BufferInfo结构就是这块显存的描述:
struct BufferInfo {
void* virAddr; // 虚拟地址(CPU 写这里)
void* phyAddr; // 物理地址(DMA 用)
int16_t width;
int16_t height;
int16_t stride; // 每行字节数(可能 > width*4 对齐)
int32_t mode; // ARGB8888 等像素格式
};
九、一次完整渲染的时序
把上面 7 层串起来,一次从 JS 到屏幕的完整流程:
① 应用启动
JS 引擎执行 index.js → 拿到 data
AppStyleManager 解析 CSS
Component::BuildViewTree() 构建组件树
↓
② 创建 Native 视图树
DivComponent → UIViewGroup
TextComponent → UILabel
ListComponent → UIList
↓
③ 布局
RootView::Measure() → FlexLayout::LayoutChildren()
计算每个 UIView 的 rect_(x/y/w/h)
(自适应高度在这里:isAutoHeight 时 SetHeight(maxChildHeight))
↓
④ 首次渲染
WindowImpl::Render()
→ UpdateHalDisplayBuffer() (Lock 拿 framebuffer)
→ rootView_->Render() (画脏区像素到显存)
WindowImpl::Flush()
→ surface->Unlock() (提交,驱动刷屏)
↓
⑤ 运行期内容变化(如切换亮度模式,增删 slider)
JS 数据变化 → HandleChildrenChange()
→ 重建子组件 + 重新布局
→ 手动重算 listItem 高度
→ OnItemSizeChanged() 通知 UIList 平移后续 item
→ Invalidate() 标记脏区
WindowImpl::Render() 只重绘脏区
Flush() 提交
↓
⑥ 屏幕显示最新画面
十、增量渲染的价值
嵌入式设备 CPU 弱、内存小,全屏 800×480 ARGB8888 一帧就是 8004804 ≈ 1.5MB,每帧全量重绘代价极高。
脏区机制让只有变化的那一小块区域被重新绘制。比如亮度切换时,只有滑块和壁纸那几十行像素需要重绘,其余区域完全不动。
更多推荐
所有评论(0)