本文章完整讲解一条 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 标签          ComponentNative 视图
─────────         ─────────────────           ────────────────
<div>        →    DivComponentUIViewGroup
<text>       →    TextComponentUILabel
<list>       →    ListComponentUIList
<list-item>ListItemComponentUIViewGroup
<slider>     →    SliderComponentUISlider

关键基类 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),负责:

  1. 分配显存:通过 Surface->Lock()拿到一块物理连续内存的虚拟地址(framebuffer)
  2. 像素写入:上层把 ARGB 值写进这块内存
  3. 提交刷新: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 视图树
   DivComponentUIViewGroup
   TextComponentUILabel
   ListComponentUIList
        ↓
③ 布局
   RootView::Measure() → FlexLayout::LayoutChildren()
   计算每个 UIViewrect_x/y/w/h)
   (自适应高度在这里:isAutoHeightSetHeight(maxChildHeight))
        ↓
④ 首次渲染
   WindowImpl::Render()UpdateHalDisplayBuffer()   (Lockframebuffer)rootView_->Render()        (画脏区像素到显存)
   WindowImpl::Flush()surface->Unlock()          (提交,驱动刷屏)
        ↓
⑤ 运行期内容变化(如切换亮度模式,增删 sliderJS 数据变化 → HandleChildrenChange()
     → 重建子组件 + 重新布局
     → 手动重算 listItem 高度
     → OnItemSizeChanged() 通知 UIList 平移后续 itemInvalidate() 标记脏区
   WindowImpl::Render() 只重绘脏区
   Flush() 提交
        ↓
⑥ 屏幕显示最新画面

十、增量渲染的价值

嵌入式设备 CPU 弱、内存小,全屏 800×480 ARGB8888 一帧就是 8004804 ≈ 1.5MB,每帧全量重绘代价极高。

脏区机制让只有变化的那一小块区域被重新绘制。比如亮度切换时,只有滑块和壁纸那几十行像素需要重绘,其余区域完全不动。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐