Skip to content

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 后将结果同步到 PixelMapdrawImage 上屏
SouiAbsLayout 绝对布局容器。为 SOUI 窗口提供 (x, y, width, height) 绝对定位
UiThreadUtils 线程工具。提供向 JS(UI)线程投递任务的能力
NativeEditView 原生输入组件。需要系统输入法体验时使用
AudioPlayer 音频播放。对应 Win32 PlaySound

C++ 层核心类

类/文件 职责
OhosPlatformAPI OHOS 平台 API 单例。提供窗口、定时器、消息、剪贴板等全量实现;由 OhosPlatformAPIReg.cpp 填充到 swinx 的 C 结构体 PlatformAPIswinx/include/platform_api.h)并调用 PlatformAPI_Init 完成注册;内含 JS 线程判定与跨线程桥
SouiSurfaceProxy C++ 端 Surface 代理。持有 INativeWindownapi_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 采用"渲染区域单一数据源"的设计:

  1. ArkTS 分配 Uint8ClampedArrayImageData.data)作为像素缓冲;
  2. C++ 经 N-API 取到缓冲区指针,包装为 HBITMAP + HDC
  3. C++ 投递 WM_PAINT,SOUI 窗口过程渲染到该 HDC;
  4. C++ 做 BGRA→RGBA 通道交换(ArkUI 为 RGBA_8888);
  5. C++ 通过 GetUpdateRgn / GetRgnBox 算出本次真正的更新矩形 rcUpdate 并返回
  6. 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_jsThreadIdinit 时记录 m_env 所属线程,之后用 isJsThread() 判定;
  • 主线程零跨线程开销(省去参数构造与结果回传),工作线程靠 napi_threadsafe_function 安全回投;
  • callBridge 内部自身也会校验线程,两条分支各自闭环;
  • 记录/擦除定时器回调(m_timerEntries)的加锁必须放在桥调用之后,否则持锁等待 JS 线程时,JS 线程上的 onTimerExpired 也要取同一把锁,会造成死锁。

4. 输入与输入法

  • 触摸:ArkUI TouchEventsurf_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.idxxmlimagevalues 等)被打进 HAP 的 rawfile 目录。SOUI 为此提供了一个专属于鸿蒙IResProvider 实现:SResProviderOhosRawFileSOUI/include/res.mgr/SResProviderOhosRawFile.h,实现在 SOUI/src/res.mgr/SResProviderOhosRawFile.cpp)。

为什么需要它

  • rawfile 资源没有"文件系统中的绝对路径",只能用 NDK 的 NativeResourceManager(配合 rawfile/raw_file_manager.h)打开。
  • 直接从 HAP 的 rawfile 读取资源,无需先把 uires 拷贝到沙箱 filesDir —— 既省磁盘空间,又省启动时的拷贝时间。
  • 与桌面的 SResProviderFilesSResProviderPESResProviderZip / 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_mapFilesSMap<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.idxNativeResourceManager 由平台层创建,本类不负责释放。
  • 需要"可写 / 随版本更新"的资源(如主题热更新文件)走沙箱 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 Cairoswinx/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 的鸿蒙适配把鸿蒙收敛进了既有的跨平台抽象,而非把框架"移植"到一个新平台:

  1. 完整的 Win32 窗口语义:HWND、消息、定时器、焦点、捕获等核心能力完整还原
  2. 零查表的 HWND:以 napi_ref 指针直接作为句柄
  3. 高效的渲染路径:离屏渲染 + C++ 单一数据源的脏矩形 + GPU 纹理上屏
  4. 跨线程安全:主线程快路径与工作线程 threadsafe_function 回投相结合
  5. 统一业务代码:Windows / Android / iOS / OHOS 共用同一份 C++ 业务代码

参考 Android 适配指南iOS 适配指南 可了解其他移动端的实现差异。