以HarmonyOS为主角的跨端框架---ACF开发实践
大家好,这里 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
回调集合覆盖的是整个生命周期:
| 回调 | 什么时候来 | 通常做什么 |
|---|---|---|
OnSurfaceCreated | surface 就绪,拿到 OHNativeWindow* | 创建渲染面、初始化 |
OnSurfaceChanged | 尺寸变化 | 更新 viewport / 重新布局 |
OnSurfaceDestroyed | surface 销毁 | 释放 GPU 资源 |
OnDispatchTouchEvent / 鼠标事件 | 用户输入 | 命中测试、分发 |
| 轴事件 | 滚轮 / 捏合 | 缩放、滚动 |
这套机制让 ArkTS 侧完全退出渲染路径:不需要每一帧跨边界来回,触摸也不必经过 ArkTS 再转发给 native。文件头的原话是 "zero NAPI round-trips"。
四、为什么我最终这么用
这里要交代一个取舍:XComponent 给了你 surface,但没给你 UI 框架。
它不负责布局、不负责命中测试、不负责动画、不负责帧调度,连"一帧什么时候画"都要你自己定。所以当你选择"整棵 UI 树都在 native 画"时,实际上是在选择:
-
自己实现 UI 框架——布局、控件、事件、动画、滚动、文本;
-
或者只把 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 画 —— 高成本,换来帧时间的完全掌控。
感谢大家看到这里,我们下次再见
更多推荐



所有评论(0)