SOUI 鸿蒙(HarmonyOS / OHOS)适配指南¶
Warning
The current page still doesn't have a translation for this language.
You can read it through google translate.
概述¶
SOUI 通过 soui-ohos-lib 模块实现了完整的 HarmonyOS(OpenHarmony,下文统称 OHOS)平台适配。借助该模块,同一套 SOUI C++ 业务代码可以直接运行在鸿蒙设备上,使 SOUI 成为覆盖 Windows / macOS / Linux / Android / iOS / OHOS 的全平台 C++ UI 框架。
为什么需要专门适配¶
鸿蒙的原生开发语言是 ArkTS、UI 体系是 ArkUI,这与 C++ 之间隔着三道坎:
| 挑战 | 说明 | SOUI 的应对 |
|---|---|---|
| 语言鸿沟 | ArkTS 带 GC,C++ 无法直接持有 ArkTS 对象 | 通过 N-API(C 接口)双向对话 |
| 线程铁律 | napi_env 仅在 JS 线程有效,工作线程直接调用 N-API 必崩 |
主线程直接调用 / 工作线程经 napi_threadsafe_function 投递 |
| 没有 HDC | 传统"拿到设备上下文就能画"的模型不存在 | 渲染走 PixelMap 位图,自行管理离屏缓冲 |
核心设计理念¶
- 窗口系统仿真:在鸿蒙上还原 Win32 窗口模型(HWND、消息队列、SetTimer、SetCapture),SOUI 核心无需修改
- 组件树映射:每个 SOUI HWND 对应一个 ArkUI 组件,窗口层级由自定义容器(
SouiAbsLayout)管理 - 离屏渲染 + 脏矩形上屏:C++ 在像素缓冲上完成渲染,ArkTS 同步到
PixelMap后仅 flush 更新区域 - 事件桥接:ArkUI 触摸/键盘事件经 N-API 转为 Win32 消息,C++ 处理后再回调 ArkTS 重绘
架构设计¶
分层架构¶
┌─────────────────────────────────────────────┐
│ SOUI C++ 应用层 │
│ (SHostWnd, SApplication, 控件, 皮肤, 布局) │
├─────────────────────────────────────────────┤
│ swinx 抽象层 │
│ (窗口管理、消息系统、资源管理、渲染接口) │
├─────────────────────────────────────────────┤
│ OhosPlatformAPI 实现层 (C++) │
│ (HWND 创建/销毁、定时器、焦点、剪贴板、IME) │
├─────────────────────────────────────────────┤
│ N-API 桥接层 │
│ (Soui4Ohos_NAPI.cpp,导出 libsoui4ohos.so) │
├─────────────────────────────────────────────┤
│ SouiPlatformBridge (ArkTS) │
│ (单例桥接、窗口工厂、定时器调度、消息循环) │
├─────────────────────────────────────────────┤
│ ArkUI 组件树 │
│ SouiScreen → SouiWindow → SouiSurface │
│ (绝对布局、事件分发、PixelMap 渲染、焦点管理) │
└─────────────────────────────────────────────┘
关键类说明¶
ArkTS 层核心类¶
| 类/文件 | 职责 |
|---|---|
SouiPlatformBridge |
全局桥接单例。管理 HWND→组件映射、定时器(SetTimer/KillTimer)、焦点、输入捕获、剪贴板、消息调度 |
SouiScreen |
屏幕根容器,对应 GetDesktopWindow。承载所有顶层窗口,通过 screenStartup/screenShutdown 驱动 SOUI 生命周期 |
SouiWindow |
窗口容器,对应一个 HWND。内含主 Surface + 子窗口组 |
SouiSurface |
主渲染 Surface。分配像素缓冲,调用 surf_render 后将结果同步到 PixelMap 并 drawImage 上屏 |
SouiAbsLayout |
绝对布局容器。为 SOUI 窗口提供 (x, y, width, height) 绝对定位 |
UiThreadUtils |
线程工具。提供向 JS(UI)线程投递任务的能力 |
NativeEditView |
原生输入组件。需要系统输入法体验时使用 |
AudioPlayer |
音频播放。对应 Win32 PlaySound |
C++ 层核心类¶
| 类/文件 | 职责 |
|---|---|
OhosPlatformAPI |
OHOS 平台 API 单例。提供窗口、定时器、消息、剪贴板等全量实现;由 OhosPlatformAPIReg.cpp 填充到 swinx 的 C 结构体 PlatformAPI(swinx/include/platform_api.h)并调用 PlatformAPI_Init 完成注册;内含 JS 线程判定与跨线程桥 |
SouiSurfaceProxy |
C++ 端 Surface 代理。持有 INativeWindow 的 napi_ref,负责事件派发与渲染 |
OhosBridge |
ArkTS 方法引用的集中持有者(定时器、窗口操作等的 napi_ref) |
soui4ohos.h |
应用入口头文件。业务侧实现 OHOS 生命周期 |
核心机制¶
1. HWND = napi_ref 指针,零查表¶
SOUI 的 HWND 贯穿全局。在鸿蒙上它不能是 ArkTS 对象(会被 GC 回收),也不应是字符串 ID(每次回调都要查表)。
SOUI 的做法:创建窗口时用 napi_create_reference 将 ArkTS 窗口对象"固持"为 napi_ref,再把 ref 的指针地址当作 HWND 返回给 C++:
// SouiSurfaceProxy.h
// 获取 nativeId(即 napi_ref 的指针值,作为 HWND 使用)
int64_t getNativeId() const { return reinterpret_cast<int64_t>(m_ref); }
回调时 napi_get_reference_value 取回对象即可。没有哈希表、没有字符串比对,一次指针解引用完成查找,与 Android 端同构。
2. 渲染:PixelMap 离屏 + 脏矩形回传¶
鸿蒙每帧全屏提交成本很高,SOUI 采用"渲染区域单一数据源"的设计:
- ArkTS 分配
Uint8ClampedArray(ImageData.data)作为像素缓冲; - C++ 经 N-API 取到缓冲区指针,包装为
HBITMAP+HDC; - C++ 投递
WM_PAINT,SOUI 窗口过程渲染到该 HDC; - C++ 做 BGRA→RGBA 通道交换(ArkUI 为
RGBA_8888); - C++ 通过
GetUpdateRgn/GetRgnBox算出本次真正的更新矩形rcUpdate并返回; - ArkTS 把缓冲同步到
PixelMap,再用该矩形做局部drawImage上屏。
// SouiSurfaceProxy.h
// rcUpdate 输出本次实际重绘的更新区域(物理像素,窗口客户区坐标)
// 空矩形(l>=r 或 t>=b)表示本次没有需要上屏的内容
void render(napi_env env, napi_value pixelBuffer, int width, int height,
RECT *rcUpdate);
Index.d.ts 中的对应声明:
/** 渲染并返回本次实际重绘的更新区域 [l, t, r, b](物理像素);空矩形表示无需上屏 */
function surf_render(nativeId: number, pixelBuffer: Uint8ClampedArray | Uint8Array,
width: number, height: number): number[];
更新矩形由 C++ 侧作为唯一数据源产出
ArkTS 层不再自行维护脏矩形列表。这避免了"两端各记一份脏区导致对不齐、画面撕裂"的经典问题。
为什么要 drawImage 而不是 putImageData
putImageData 走软件路径,全屏约 40ms;drawImage(PixelMap) 走 GPU 纹理,可做到 <1ms。
3. 跨线程安全:先判线程,再决定是否跨线程¶
这是鸿蒙适配中最容易踩坑的一点:napi_env 只在 JS 线程有效,从工作线程直接调用 N-API 会立即崩溃——这不是偶发 bug,而是 N-API 的设计约束。
SOUI 中 setTimer 多数发生在主线程,但框架内部的工作线程也会申请定时器。因此实现为"主线程快路径 + 工作线程回退":
// OhosPlatformAPI.h
// 直接调用 ArkTS Bridge(仅允许在 JS 线程调用);非 JS 线程请使用 invokeBridge
napi_value callBridge(napi_ref methodRef, size_t argc = 0, napi_value *argv = nullptr);
enum { kBridgeWaitMs = 2000 };
bool isJsThread() const { return std::this_thread::get_id() == m_jsThreadId; }
bool invokeBridge(napi_ref methodRef, const std::vector<BridgeArg> &args,
BridgeRetType retType, BridgeResult *out, int waitMs = kBridgeWaitMs);
// OhosPlatformAPI.cpp —— 定时器族的三条路径统一成这一模式
if (isJsThread()) {
// 主线程快路径:直接 callBridge 调用 ArkTS 的 setTimer,零跨线程开销
} else {
// 工作线程:经 napi_threadsafe_function 把调用投递回 JS 线程执行
}
要点:
m_jsThreadId在init时记录m_env所属线程,之后用isJsThread()判定;- 主线程零跨线程开销(省去参数构造与结果回传),工作线程靠
napi_threadsafe_function安全回投; callBridge内部自身也会校验线程,两条分支各自闭环;- 记录/擦除定时器回调(
m_timerEntries)的加锁必须放在桥调用之后,否则持锁等待 JS 线程时,JS 线程上的onTimerExpired也要取同一把锁,会造成死锁。
4. 输入与输入法¶
- 触摸:ArkUI
TouchEvent→surf_onMotionEvent→ C++ 投递WM_LBUTTONDOWN/WM_MOUSEMOVE等消息,支持多指(POINTER_DOWN/POINTER_UP) - 捕获:使用统一的
SetCapture/ReleaseCapture,屏蔽 ArkUI 事件模型差异 - 键码映射:鸿蒙键码(2000+ 段)映射回
VK_*体系,使键盘逻辑跨端通用 - 输入法:由
imc.attach将自绘组件与系统输入法绑定;showSoftKeyboard通过 Canvas 的.id()驱动获焦 - 键盘高度:由 ArkTS 侧回调到 C++,SOUI 自行处理布局避让
5. 多窗口与 screenId¶
- 每个
SouiScreen拥有唯一的screenId,对应鸿蒙的多 Ability / 多窗口场景 - C++ 层通过
m_activeScreenStack管理活动 screen 栈 SHostWnd::Create(NULL)时依据活动栈顶路由到正确的 screen- 这与 Android 端的 screenId 激活栈机制完全一致
环境要求¶
| 工具 | 要求 |
|---|---|
| DevEco Studio | 建议使用最新稳定版 |
| HarmonyOS SDK | API 12+ |
| CMake | 3.18.1+(DevEco 自带) |
| Native 构建 | CMake + Ninja,与 Android NDK 构建体验一致 |
工程结构与编译配置¶
目录结构¶
soui4/
├── soui-ohos-lib/ # 鸿蒙适配库(har)
│ └── src/main/
│ ├── cpp/ # C++ 桥接源码
│ │ ├── include/ # OhosPlatformAPI.h、soui4ohos.h 等
│ │ ├── src/ # OhosPlatformAPI.cpp、Soui4Ohos_NAPI.cpp、
│ │ │ # SouiSurfaceProxy.cpp、OhosBridge.cpp
│ │ ├── ohos_napi_bridge.h # N-API 桥助手
│ │ └── ohos_ime_bridge.h # 输入法桥助手
│ └── ets/ # ArkTS 层
│ ├── SouiPlatformBridge.ets # 桥接单例
│ ├── SouiScreen.ets # 屏幕根容器
│ ├── SouiSurface.ets # 渲染 Surface
│ ├── SouiAbsLayout.ets # 绝对布局
│ ├── UiThreadUtils.ets # 线程工具
│ └── ... # NativeEditView / AudioPlayer 等
├── games/cnchess/client/ohos/ # 示例应用:中国象棋(含联网对战)
└── demos/ohos-demo/ # 示例应用:基础 demo
业务模块 CMakeLists¶
业务侧 CMake 需引入 SOUI 核心并编译 N-API 桥,基本形态与 Android 一致:
cmake_minimum_required(VERSION 3.18.1)
project("my-ohos-app")
# 由 build-profile / CMake 参数传入
# SOUI_ROOT_DIR - soui4 仓库根目录
# SOUI_OHOS_LIB_DIR - soui-ohos-lib/src/main 目录
add_library(my-ohos-app SHARED ${MY_SRC})
target_include_directories(my-ohos-app PRIVATE
${SOUI_OHOS_LIB_DIR}/cpp/include
${SOUI_ROOT_DIR}/SOUI/include
${SOUI_ROOT_DIR}/swinx/include
${SOUI_ROOT_DIR}/utilities/include
${SOUI_ROOT_DIR}/components
)
target_link_libraries(my-ohos-app PUBLIC
soui4ohos soui4 utilities4 swinx
libace_napi.z.so libhilog_ndk.z.so
)
N-API 模块注册¶
桥接库以 libsoui4ohos.so 形式导出,模块名为 soui4ohos:
// Soui4Ohos_NAPI.cpp
extern "C" napi_value __napi_soui4ohos_entry(napi_env env, napi_value exports) {
napi_define_properties(env, exports,
sizeof(g_soui4ohos_exports) / sizeof(g_soui4ohos_exports[0]),
g_soui4ohos_exports);
return exports;
}
ArkTS 侧通过 import soui4ohos from 'libsoui4ohos.so' 使用,类型声明由 cpp/types/libsoui4ohos/Index.d.ts 提供。
不要在业务里重命名桥接库
与 Android 一样,库名与 N-API 模块名是配套的。改名会导致 ArkTS import 失败。
权限配置¶
鸿蒙对网络访问有严格管控。若应用需要联网,必须在 module.json5 中声明 INTERNET 权限,否则 socket 创建会直接失败:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
排查要点:errno 1 (EPERM)
若使用 libwebsockets 等网络库时出现 lws_client_connect_3_connect: conn fail: skt creation: errno 1,几乎总是因为缺少 ohos.permission.INTERNET。该 errno 即 EPERM(操作不允许),并非地址或端口错误。
修改权限后必须卸载重装应用才能生效,覆盖安装不会重新授予权限。
调试与问题排查¶
日志¶
C++ 侧通过 hilog 输出,可用 SLOGI() / SLOGE() 等宏;过滤时使用桥接库输出的标签(加载时会打印 libsoui4ohos.so loaded: exports registered=N)。
常见问题¶
Q1:工作线程调用 setTimer 等 API 后崩溃¶
napi_env 只能在 JS 线程使用。请确认业务代码没有从自建线程直接调用 OhosPlatformAPI 的桥方法。框架内部的定时器已通过 isJsThread() + napi_threadsafe_function 处理,若业务自行新增桥调用,需沿用同一模式。
Q2:定时器回调不触发或死锁¶
检查是否在持有 m_timerEntries 锁的情况下调用桥方法。加锁必须在桥调用之后,否则 JS 线程上的 onTimerExpired 会与等待中的工作线程形成死锁。
Q3:画面撕裂或局部刷新错位¶
确认 ArkTS 侧没有自行维护脏矩形列表。更新矩形应完全以 surf_render 返回的 [l, t, r, b] 为准(单一数据源)。返回空矩形表示本次无需上屏,应跳过 drawImage。
Q4:界面不刷新(如窗口隐藏后再显示)¶
脏矩形机制下没有"无 dirty 则整屏兜底"的分支。若窗口由隐藏转为显示但未收到 InvalidateRect,需要在显示路径上显式触发一次失效重绘。
Q5:无法联网¶
见上文「权限配置」章节,确认 ohos.permission.INTERNET 已声明且应用已卸载重装。
Q6:中文输入异常¶
确认 NativeEditView 被正确注册;自绘编辑控件需检查输入法的 imc.attach 绑定与 commitText 是否正确回传到当前焦点 HWND。
SResProviderOhosRawFile:基于 rawfile 的资源提供器¶
鸿蒙(OHOS)没有 Windows 那样的传统文件系统路径,SOUI 的 uires 资源包(含 uires.idx、xml、image、values 等)被打进 HAP 的 rawfile 目录。SOUI 为此提供了一个专属于鸿蒙的 IResProvider 实现:SResProviderOhosRawFile(SOUI/include/res.mgr/SResProviderOhosRawFile.h,实现在 SOUI/src/res.mgr/SResProviderOhosRawFile.cpp)。
为什么需要它¶
- rawfile 资源没有"文件系统中的绝对路径",只能用 NDK 的
NativeResourceManager(配合rawfile/raw_file_manager.h)打开。 - 它直接从 HAP 的 rawfile 读取资源,无需先把
uires拷贝到沙箱filesDir—— 既省磁盘空间,又省启动时的拷贝时间。 - 与桌面的
SResProviderFiles、SResProviderPE、SResProviderZip/SResProvider7Z是同一级抽象的不同实现,后端换成了NativeResourceManager。
关键实现要点(源码核对)¶
Init(WPARAM, LPARAM):wParam=(NativeResourceManager*)鸿蒙原生资源管理器(由OH_ResourceManager_InitNativeResourceManager(env, jsResMgr)取得,通常在 N-API 侧完成)。lParam=rawfile下的前缀目录名,如_T("uires")或_T("soui_sys_res")。uires.idx约定:与 Android 完全一致 —— 前缀目录下必须有标准uires.idx,索引里的path="uidef\\init.xml"经NormalizeAssetPath标准化为uidef/init.xml,再拼前缀得到 rawfile 相对路径uires/uidef/init.xml。- 路径缓存:
m_mapFiles(SMap<SResID, SStringT>)缓存type+name → 规范化后的 rawfile 相对路径;strType == nullptr时把pszResName直接当相对路径拼前缀。 - 所有权:
m_resMgr由平台层(OH_ResourceManager_InitNativeResourceManager)创建,本类不拥有、也不释放;OpenRawFile返回RawFile*,调用方负责OH_ResourceManager_CloseRawFile。 - 其余
IResProvider接口与SResProviderFiles对齐,业务代码无需区分资源来源。
用法¶
最简方式是用 SAppCfg 的专属助手(内部 CreateResProvider(RES_OHOS_RAWFILE) 并 Init),见 SOUI/include/SAppCfg.h:
// games/cnchess/client/ohos_entry.cc
cfg.SetRender(Render_Skia);
cfg.SetSysResOhosRawFile(resMgr, _T("soui_sys_res")); // 系统资源
cfg.SetAppResOhosRawFile(resMgr, _T("uires")); // 应用资源
也可手动创建并挂到资源树(接口与桌面 SResProviderFiles::Init 对齐,但参数类型不同):
#include <rawfile/raw_file_manager.h>
NativeResourceManager* resMgr =
OH_ResourceManager_InitNativeResourceManager(env, jsResMgr);
SResProviderOhosRawFile* p = new SResProviderOhosRawFile();
p->Init((WPARAM)resMgr, (LPARAM)_T("uires"));
GETRESPROVIDER->AddResProvider(p, _T("uidef:xml_init")); // 或 LoadSystemNamedResource
与桌面 ResProvider 对照
| 提供器 | 后端 | 典型平台 |
|---|---|---|
SResProviderFiles |
磁盘目录 | Windows / Linux / macOS |
SResProviderPE |
PE 资源段 | Windows |
SResProviderZip / SResProvider7Z |
压缩包 | 跨平台 |
SResProviderOhosRawFile |
HAP rawfile(NativeResourceManager) | 仅 OHOS |
注意
- 前缀目录必须落在
rawfile下且含uires.idx;NativeResourceManager由平台层创建,本类不负责释放。 - 需要"可写 / 随版本更新"的资源(如主题热更新文件)走沙箱
filesDir,不要放进 rawfile(rawfile 是只读的)。
各平台差异对比¶
| 方面 | Windows | Android | iOS | OHOS |
|---|---|---|---|---|
| 桥接技术 | 原生 | JNI | Objective-C++ 直编 | N-API |
| swinx 平台层 | 不适用(用系统 Win32) | swinx/src/platform/mobile |
swinx/src/platform/ios |
swinx/src/platform/mobile |
是否用 platform_api |
否 | 是 | 否(swinx 内部自实现) | 是 |
| HWND 载体 | 真实窗口句柄 | jobject GlobalRef 地址 |
SUIView 指针 |
C++ native 句柄,ArkTS 侧经 mWindowMap 反查 |
| swinx 2D 后端 | 不适用(系统 GDI) | Cairo | Core Graphics | Cairo(swinx/src/gdi/cairo) |
| SOUI 渲染工厂 | Render_Skia / Render_Gdi / Render_D2d |
Render_Skia |
Render_Skia |
Render_Skia |
| 上屏方式 | 系统合成 | canvas.drawBitmap |
CGContext 绘制 |
drawImage(PixelMap) |
| 脏矩形 | 系统更新区 | Java 侧维护 | 系统更新区 | C++ 回传 rcUpdate |
| 消息循环 | 原生 GetMessage |
Handler 调度 | CFRunLoop 驱动 |
ArkTS 调度 + scheduleMessageProcessing |
| 定时器 | 系统 SetTimer |
Handler.postDelayed |
C++ 消息循环驱动 TimerInfo |
ArkTS setTimer |
| 跨线程约束 | 无 | JavaVM Attach | 主线程 UI 约束 | napi_env 仅 JS 线程 |
| 资源 | 文件系统 | AssetManager | Bundle | rawfile + 沙箱路径 |
文件索引¶
| 用途 | 路径 |
|---|---|
| 平台 API 实现 | soui-ohos-lib/src/main/cpp/src/OhosPlatformAPI.cpp |
| 平台 API 头文件 | soui-ohos-lib/src/main/cpp/include/OhosPlatformAPI.h |
| N-API 导出 | soui-ohos-lib/src/main/cpp/src/Soui4Ohos_NAPI.cpp |
| Surface 代理(渲染/脏矩形) | soui-ohos-lib/src/main/cpp/src/SouiSurfaceProxy.cpp/.h |
| ArkTS 桥接单例 | soui-ohos-lib/src/main/ets/SouiPlatformBridge.ets |
| ArkTS 渲染组件 | soui-ohos-lib/src/main/ets/SouiSurface.ets |
| ArkTS 屏幕容器 | soui-ohos-lib/src/main/ets/SouiScreen.ets |
| 示例:中国象棋 | games/cnchess/client/ohos/ |
| 示例:基础 demo | demos/ohos-demo/ |
总结¶
SOUI 的鸿蒙适配把鸿蒙收敛进了既有的跨平台抽象,而非把框架"移植"到一个新平台:
- 完整的 Win32 窗口语义:HWND、消息、定时器、焦点、捕获等核心能力完整还原
- 零查表的 HWND:以
napi_ref指针直接作为句柄 - 高效的渲染路径:离屏渲染 + C++ 单一数据源的脏矩形 + GPU 纹理上屏
- 跨线程安全:主线程快路径与工作线程
threadsafe_function回投相结合 - 统一业务代码:Windows / Android / iOS / OHOS 共用同一份 C++ 业务代码
参考 Android 适配指南 与 iOS 适配指南 可了解其他移动端的实现差异。