大家好,这里 Aaron。上次简单聊过我在做 ACF 跨端框架,这次开始系统地拆解。

第一篇只讲一件事:XComponent 怎么用。这个话题看起来基础,但它的用法几乎决定了一个鸿蒙应用的项目形态。我最初照着 Qt 鸿蒙化模板的工程结构理解它,也踩过不少偏差,这里梳理一下。

在动手前自己拆解了一下Qt鸿蒙化工程模板的思路,简单来说就是通过XComponent作为窗口显示native侧的实现。而native侧的api则通过动态加载库进行封装,


一、先把 XComponent 说清楚

XComponent 是 ArkUI 里专门用于自绘制的组件。它的本质是:

向 native 侧交出一块 surface(OHNativeWindow*),native 侧在这块 surface 上自己绘制。

所以它不是"一个显示 C++ 内容的容器",也不需要 C++ 生成什么 UI 树交给 ArkUI。它交出的是像素缓冲区,ArkUI 只负责把这块 surface 合成到界面上。

这一点值得多说一句,因为很多人(包括我)第一次接触时会误以为它是"native UI 的容器"。其实官方的 libraryname 机制让 native 侧可以自注册 XComponent 模块、直接接管整个生命周期:surface 创建、尺寸变化、销毁、触摸、鼠标、轴事件,全部交给 native。ArkTS 侧只剩一个声明。


二、最简单的一种用法

ArkTS 里只有这一行:

XComponent({
  id: 'acfmlview',
  type: XComponentType.SURFACE,
  libraryname: 'acf'
})
  • type: XComponentType.SURFACE —— 要的是一块 surface;

  • libraryname: 'acf' —— 让系统按名字加载 libacf.so,并在其中查找 XComponent 模块的注册入口。

关键点:ArkTS 侧不需要写任何回调。 不需要注册 onSurfaceCreated、不需要把 nativeWindow 传下去、不需要接触摸事件。这些都在 native 侧由自注册的模块接管。

这就是"只给一个声明,控制权全在 native"的用法。


三、native 侧要做什么

native 侧的入口是系统约定的导出符号,名字由 libraryname 决定。库被加载后,系统调用它拿 module 对象:

// acf/src/bridge/acfml_bridge.cpp(简化示意)
napi_value OH_NativeXComponent_RegisterCallback(...);  // 系统的注册接口

EXTERN_C_START
napi_value Init(napi_env env, napi_value exports) {
    OH_NativeXComponent* component = ...;   // 取出 XComponent 实例
    OH_NativeXComponent_RegisterCallback(component, &g_callbacks);
    return exports;
}
EXTERN_C_END

回调集合覆盖的是整个生命周期:

回调什么时候来通常做什么
OnSurfaceCreatedsurface 就绪,拿到 OHNativeWindow*创建渲染面、初始化
OnSurfaceChanged尺寸变化更新 viewport / 重新布局
OnSurfaceDestroyedsurface 销毁释放 GPU 资源
OnDispatchTouchEvent / 鼠标事件用户输入命中测试、分发
轴事件滚轮 / 捏合缩放、滚动

这套机制让 ArkTS 侧完全退出渲染路径:不需要每一帧跨边界来回,触摸也不必经过 ArkTS 再转发给 native。文件头的原话是 "zero NAPI round-trips"。


四、为什么我最终这么用

这里要交代一个取舍:XComponent 给了你 surface,但没给你 UI 框架。

它不负责布局、不负责命中测试、不负责动画、不负责帧调度,连"一帧什么时候画"都要你自己定。所以当你选择"整棵 UI 树都在 native 画"时,实际上是在选择:

  1. 自己实现 UI 框架——布局、控件、事件、动画、滚动、文本;

  2. 或者只把 XComponent 用于少量自绘区域(波形、图表、游戏画布),其余 UI 继续用 ArkUI。

这两种用法都合理。ACF 选了第一种,因为它是音频应用:界面是交互密集 + 大量自绘(波形、时间线、推子)。但代价必须诚实:一个框架要长期维护布局、输入法、文本选择这些东西,成本是被系统性低估的。


五、几个容易忽略的细节

1. libraryname 是按名字 dlopen 的。 所以产出的 .so 必须能被按名找到——它的 DT_SONAME 要正确。CMake 在 OHOS 上不认 SOVERSION,产出的 .so 可能**完全没有 DT_SONAME**,这就成了一个独立的问题,后面单独讲。

2. 用 id 可以区分多个 XComponent。 id 不是只能写死——可以从本窗口的 LocalStorage 读出来。这样同一个页面实例在不同窗口里得到不同的 id,native 侧就能按 id 选择要渲染哪份内容。一个页面服务任意多个窗口,这个能力对桌面级应用很关键。

3. 非 1:1 缩放是常态。 设备尺寸和设计稿尺寸不一致时,坐标换算是一个必须严肃对待的问题——在哪一层换算、由谁负责,只有一个正确答案。这也会单独讲。

4. 自注册代码要能被链接器保住。 如果 native 库靠静态初始化器自注册,普通链接会丢掉整个目标文件:.so 加载成功、运行正常、surface 一片空白、日志一个字不说。这不是 XComponent 的问题,但它是 XComponent 用法里最容易被忽略的一环。


小结

XComponent 的正确理解是:它不是容器,是 surface。

  • ArkTS 侧:一个声明,SURFACE + libraryname;

  • native 侧:自注册模块,接管生命周期和输入;

  • 中间:没有 UI 框架,布局和绘制是你的责任。

想清楚这一点之后,"要不要用它"就变成了一个架构选择,一个 API 用法问题:

  • 只用于自绘区域 —— 低成本,保留 ArkUI 生态;

  • 整棵 UI 在 native 画 —— 高成本,换来帧时间的完全掌控。

感谢大家看到这里,我们下次再见

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐