OpenHarmony 图形子系统:ui_lite ↔ ace_engine_lite 对接机制
·
一、为什么有两套 UI?先分清分工
很多南向开发者第一次看源码会困惑:明明已经有 UIView 了,为什么还要 Component?
表格
| 维度 | ace_engine_lite | ui_lite |
|---|---|---|
| 定位 | JSUI 应用框架(前端运行时) | 轻量级图形渲染引擎(后端绘制) |
| 语言 | C++ + JerryScript(JS 引擎) | 纯 C++ |
| 抽象层级 | 声明式:new Text('hello') | 命令式:label->SetText("hello") |
| 职责 | 解析 JS、管理组件树、计算样式/布局、事件分发调度 | 建一棵 UIView 树、标脏、Draw、上屏 |
| 生命周期 | 随 JS Page 创建/销毁 | 随 RootView 挂载,帧驱动刷新 |
| 你改哪层 | 加新组件、改事件模型、改样式属性 | 加新控件、改绘制效果、改输入处理 |
关键认知:ace_engine_lite 的 Component 不是 UIView 的子类,它是 UIView 的持有者与管理者。一个 Component 内部可以持有多个 UIView(比如一个自定义组件包含一个背景 View + 一个文字 View),但对外只暴露一个根节点 —— GetComponentRootView()。
┌───────────── JS 层 ─────────────┐
new Button({text:'OK'})
│ createElement
┌───────── ace_engine_lite ───────┐
ButtonComponent ◄──► nativeElement_ (JerryScript 对象)
│ Create()
┌────────── ui_lite ──────────────┐
UILabelButton ──Draw──► buffer ──► 屏幕
⚠️ 这条链路上任何一环断了,表现都是「页面空白但没报错」——因为 JS 层异常往往被框架吞掉,只能靠逐层打点定位。
二、组件映射表:ComponentFactory 的注册表
ComponentFactory 是整条链路的唯一入口。JS 里 createElement('button') 最终会走到这个工厂的 switch/case 或注册表,返回对应的 Component,Component 的 Create() 再去 new 出 UIView。
表格
| ace_engine_lite Component | ui_lite UIView | 对应关系与实现要点 |
|---|---|---|
DivComponent | UIViewGroup | 最基础的容器,对应 div;只负责排布子节点,自己不画东西 |
StackComponent | UIViewGroup | 层叠容器,子节点按添加顺序 Z 序叠加;注意后 Add 的在上层 |
TextComponent | UILabel | 文本显示;字体/字号/颜色走 Style 解析,缺字是字库裁剪问题 |
ImageComponent | UIImageView | 图片;src 路径由资源管理器转成 pixelmap,大图解码是 OOM 高发点 |
ListComponent | UIList | 可滚动列表;必须开复用,否则长列表必卡 |
SwiperComponent | UISwipeView | 滑动翻页;snap to page,loop 模式首尾衔接需实测 |
SliderComponent | UISlider | 滑块;拖动事件要消费掉,避免穿透到下层滚动容器 |
SwitchComponent | UIToggleButton | 开关;状态切换后记得 Invalidate() |
CheckboxComponent | UICheckBox | 复选框 |
RadioComponent | UIRadioButton | 单选;互斥逻辑在 Component 层(RadioGroup)实现,UIView 只管画 |
ButtonComponent | UIButton / UILabelButton | 有图用 UIButton,纯文字用 UILabelButton |
InputEditTextComponent | UIEditText | 输入框;焦点全局唯一,切焦点要收起旧键盘/停光标动画 |
ProgressComponent | UIBoxProgress / UICircleProgress | 横向条形 / 环形;方向由属性决定 |
ChartComponent | UIChart | 图表;能力基础,点数多会卡,大数据量建议自绘 |
CanvasComponent | UICanvas | 画布;给 JS 暴露自绘入口 |
PickerComponent | UIPicker | 选择器;多列联动时避免回调里同步刷新导致递归 |
AnalogClockComponent | UIAnalogClock | 模拟时钟;指针旋转常用 UITextureMapper 配合 |
VideoComponent | UIVideo | 视频容器;实际解码播放走多媒体子系统 |
MarqueeComponent | UILabel(marquee 模式) | 跑马灯;本质是 UILabel + 逐帧偏移动画 |
ScrollLayer | UIViewGroup + UIScrollView 包裹 | 页面级滚动层;整个 Page 内容包在一个 ScrollView 里 |
三、对接机制核心:三层双向绑定
这是本节最重要的部分,也是你能在源码里抓住的唯一一根「线」。
3.1 代码锚点
// component.cpp:164-165(行号随版本变动,以你本地为准)
jerry_set_object_native_pointer(nativeElement_, this, nullptr);
这一行把三样东西缝在了一起:
┌─────────────────────────────────────────────┐
│ jerry_value_t nativeElement_ (JS 对象) │ ← JS 侧拿到的 element
│ ↕ jerry_set_object_native_pointer │
│ Component* this (C++ 对象) │ ← 逻辑与生命周期管理者
│ ↕ GetComponentRootView() │
│ UIView* rootView_ (渲染节点) │ ← 树上真实存在的那一个
└─────────────────────────────────────────────┘
3.3 「双向绑定」的工程含义
表格
| 特性 | 具体表现 | 风险 |
|---|---|---|
| 三层强耦合 | 任意一层悬空 = 崩溃或静默失效 | 销毁顺序必须是 JS 解绑 → Component 析构 → UIView 从父节点 Remove → delete |
| 原生指针无所有权语义 | void* 不携带生命周期信息 | dangling pointer;UIView 活着但 Component 已死,一点就崩 |
| 无 GC 联动 | finalize 为 nullptr | 必须在 Page/组件 onDestroy 里成对清理 |
| 跨层访问违规 | 在非 UI 线程通过 elementPtr 调 JS API | JerryScript 非线程安全,直接 abort;所有 UI 操作收拢 UI 线程(ui_lite 的硬约束,这里同样适用) |
四、完整链路走一遍:从 new Button() 到屏幕上出现一个按钮
4.1 创建阶段
1. JS: new Button({text: 'OK', x: 10, y: 20})
2. ace: createElement('button') → ComponentFactory::Create(TAG_BUTTON)
3. ace: new ButtonComponent() → component->Create()
4. ui: new UILabelButton() ← UIView 诞生
5. ace: component->SetUIView(labelButton)
6. ace: jerry_set_object_native_pointer(nativeElement_, this, nullptr)
7. ui: rootView_->Add(labelButton) ← 上树,从此每帧被 Draw
8. ui: labelButton->Invalidate() ← 首帧标脏
第 7 步之前,UIView 画了也白画(不在树上,RootView 遍历不到)。
第 6 步漏了,事件回不来。这两条是排查新建组件不显示/不响应的唯二检查点。
4.2 属性更新阶段(setState 之后)
JS setState
→ ace diff 属性(Style/Attr 对比)
→ Component::Update(attr) ← 只更新变化的属性
→ UIView->SetText() / SetPosition() ...
→ UIView->Invalidate() ← ★ 必须标脏,否则画面不动
→ 下一帧 RootView::Draw(脏区)
4.3 事件回流阶段(最容易断的一环)
触摸按下
→ InputManager::DispatchEvent
→ RootView::DispatchTouchEvent
→ UIViewGroup::DispatchEvent(逆序遍历子节点,上层优先)
→ UIView::OnEvent(event)
├─ hit test:点是否在 rect 内
├─ 记录按下态 → Invalidate()(视觉反馈)
└─ 抬起时:
if (onClickListener_) onClickListener_->OnClick(this);
↓
ViewExtraMsg.elementPtr → nativeElement_
↓
CallJSFunction(nativeElement_, "onClick", args) ← 跨到 JS
↓
JS 的 onClick() 执行
五个高频断点:
- OnEvent 返回了 false → 事件继续冒泡,被父容器吃掉(滚动容器尤其容易)。可交互控件消费事件必须
return true。 - hit test 坐标算错 → 相对坐标/绝对坐标混用,或者父容器有 scroll offset 没补偿。
- 容器没裁剪也没消费 → 子控件画出界,点在可视区域外却还能点到。
- elementPtr 为空 → 跨层回调找不到 JS 对象,表现为「UI 有反应(按下态变了)但 JS 回调没执行」。
- JS 回调里抛异常 → JerryScript 异常若不检查
jerry_value_is_error,后续事件全部静默丢失。
五、动手:给系统加一个自定义组件(完整对照模板)
假设要加一个 Knob(旋钮)组件,需要在两层各写一份代码,并在工厂注册。
5.1 ui_lite 侧:UIView 子类(负责画 + 响应触摸)
// foundation/arkui/ui_lite/frameworks/components/ui_knob.h
class UIKnob : public UIView {
public:
UIKnob() { SetWidth(80); SetHeight(80); }
void SetValue(int v);
int GetValue() const { return value_; }
using OnChanged = void (*)(UIKnob&, int);
void SetOnChanged(OnChanged cb) { onChanged_ = cb; }
bool OnEvent(const Event& event) override;
void Draw(const Rect& area) override;
private:
int value_ = 0;
OnChanged onChanged_ = nullptr;
};
要点回顾:属性改了必 Invalidate();Draw 先做矩形相交裁剪;拖拽消费事件 return true。(细节见社区《ui_lite 组件层详解》的自定义组件模板)
5.2 ace_engine_lite 侧:Component 子类(负责接 JS 参数 + 持有 UIView)
// foundation/ace/ace_engine_lite/frameworks/components/knob_component.h
class KnobComponent : public Component {
public:
explicit KnobComponent(const char* name) : Component(name) {}
~KnobComponent() override = default;
// ★ 核心:创建并返回根 UIView
UIView* GetComponentRootView() const override { return rootView_; }
// ★ 接收 JS 侧属性
bool SetPrivateAttribute(const char* attrName, jerry_value_t attrValue) override;
// ★ 事件回传到 JS
void OnValueChanged(int value);
private:
UIKnob* rootView_ = nullptr;
};
// knob_component.cpp
UIView* KnobComponent::CreateView()
{
rootView_ = new UIKnob();
// 建立反向绑定(字段名以你本地 ViewExtraMsg 定义为准)
auto* extra = rootView_->GetViewExtraMsg();
if (extra != nullptr) {
extra->elementPtr = &nativeElement_; // ★ 指向 nativeElement_ 的地址
}
return rootView_;
}
bool KnobComponent::SetPrivateAttribute(const char* attrName, jerry_value_t attrValue)
{
if (strcmp(attrName, "value") == 0) {
int v = static_cast<int>(jerry_get_number_value(attrValue));
if (rootView_ != nullptr) {
rootView_->SetValue(v); // 只改属性,Create 后才能调
}
return true;
}
return Component::SetPrivateAttribute(attrName, attrValue);
}
void KnobComponent::OnValueChanged(int value)
{
// ★ 回调 JS:通过 nativeElement_ 找到 JS 侧绑定的 onchanged
CallJSMethod(nativeElement_, "onchanged", value); // 框架封装,注意错误检查
}
5.3 注册到工厂
// component_factory.cpp
Component* ComponentFactory::CreateComponent(const char* componentName, ...)
{
if (strcmp(componentName, "knob") == 0) {
auto* comp = new KnobComponent(componentName);
comp->CreateView(); // 有的版本在 Add 时懒创建,按你本地写法来
return comp;
}
// ...原有分支
}
验证三连(缺一不可)
// JS 侧
import router from '@system.router';
export default {
render() { /* hml 里 <knob value="30" onchanged="onTurn"></knob> */ },
onTurn(e) { console.info('knob: ' + e.value); }
}
- 显示:屏幕出现旋钮 → 工厂注册 +
CreateView+Add到树都通了。 - 更新:改
value属性画面跟着变 → diff →SetPrivateAttribute→Invalidate通了。 - 交互:拖动打印日志 →
OnEvent消费 +elementPtr反向绑定 + JS 回调通了。
六、边界:什么不该在哪层做
| 想做的事 | 正确归属 |
|---|---|
| 新增一个控件的视觉效果 | ui_lite 新增 UIView 子类 |
| 新增一个 JS 可调用的组件/属性 | ace 新增 Component + 工厂注册 + 属性解析 |
| 改交互逻辑(互斥、联动、数据绑定) | ace 层 Component |
| 改绘制原语(画圆/画字/抗锯齿) | ui_lite 的 draw/ 模块 |
| 改布局算法 | ui_lite 的 layout/(FlexLayout 等)+ ace 的布局计算 |
| 改帧率、双缓冲、上屏合成 | window_manager_lite / surface / engines |
| 跨进程通信、状态持久化 | 不属于这两层,走应用框架 |
更多推荐

所有评论(0)