Skip to content

SOUI iOS 适配指南

Warning

The current page still doesn't have a translation for this language.

You can read it through google translate.

概述

SOUI 通过 swinx/src/platform/ios 平台层实现了 iOS 支持。与 Android(JNI 桥)和鸿蒙(N-API 桥)不同,iOS 端没有独立的桥接库,而是直接以 Objective-C++ 对接 UIKit 与 Core Graphics。

由于 iOS 与 macOS 同属 Apple 生态且共用部分 Darwin 基础,SOUI 的 iOS 适配与桌面端共享同一套入口模型——这也是 SOUI 能用同一份 C++ 业务代码覆盖 Windows / macOS / Linux / Android / iOS / OHOS 的原因之一。

核心设计理念

  • 窗口系统仿真:HWND 直接映射为 UIView 子类(SUIView)的桥接指针,在其上还原 Win32 窗口语义
  • 无桥接层:Objective-C++ 可直接混编调用 UIKit,无需 JNI / N-API 之类的跨语言桥
  • 消息循环接管:以 CFRunLoop 驱动 GetMessage 循环,替代 UIApplicationMain 内部的 CFRunLoopRun()
  • 定时器自驱动:定时器由 C++ 消息循环自行调度,不依赖 NSTimer
  • 统一入口:iOS 与桌面共用同一个 _tWinMain,仅通过 swinx_ios_entry 包装接入

架构设计

分层架构

┌─────────────────────────────────────────────┐
│              SOUI C++ 应用层                  │
│  (SHostWnd, SApplication, 控件, 皮肤, 布局)   │
├─────────────────────────────────────────────┤
│              swinx 抽象层                     │
│  (窗口管理、消息系统、资源管理、渲染接口)       │
├─────────────────────────────────────────────┤
│       swinx iOS 平台层 (Objective-C++)        │
│  SConnection(消息循环/定时器)                  │
│  SUIWindow(窗口) SClipboard(剪贴板)           │
│  SImContext(输入法) atoms imm ole2 ...        │
├─────────────────────────────────────────────┤
│       UIKit + Core Graphics                   │
│  UIView / UIWindow / UIApplication            │
│  CGContextRef / UIPasteboard                  │
└─────────────────────────────────────────────┘

与 Android / 鸿蒙的关键差异

Android 与鸿蒙都需要在"原生语言层"(Java / ArkTS)与 C++ 之间架桥,因此各自有 soui-android-lib / soui-ohos-lib。 iOS 的 Objective-C++ 可以直接混编调用 UIKit,所以不存在桥接层,平台实现直接落在 swinx/src/platform/ios

关键类说明

类/文件 职责
SConnection 消息循环核心。基于 CFRunLoop 驱动消息队列,管理定时器链表与唤醒源
SUIWindow.h 窗口桥接接口。声明 createUiWindow / showUiWindow / invalidateUiWindow 等,HWND 即 SUIView 指针
SUIView UIView 子类,承载一个 SOUI HWND
SClipboard 剪贴板。基于 UIPasteboard
SImContext / imm.mm 输入法相关
keyboard.h/.mm 键盘与键码映射
atoms.h/.mm Atom 表(窗口类注册等)
ios_main.mm iOS 应用入口。定义 SwinxAppDelegate 并暴露 swinx_ios_entry()
dlghelper.mm 原生对话框辅助(如 UIDocumentPicker
STrayIconMgr 托盘图标管理(iOS 上为适配存根)

核心机制

1. 入口设计:绕开 UIApplicationMain 的阻塞

UIApplicationMain阻塞调用,它会启动 main runloop 且永不返回。因此不能简单地"先调 UIApplicationMain 再调 _tWinMain"。

SOUI 的处理方式(swinx/src/platform/ios/ios_main.mm):

  1. AppDelegatedidFinishLaunching 中,用 dispatch_async 异步调用宿主程序提供的 _tWinMain,让 didFinishLaunching 先返回;
  2. 此时 UIApplication 已创建、UIKit 事件系统就绪;
  3. _tWinMain 内部的 GetMessage 循环通过 CFRunLoopRunInMode 运行 main runloop,从而替代 UIApplicationMain 内部的 CFRunLoopRun(),正常接收 UIKit 触摸事件。
// ios_main.mm(节选)
- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 使用 dispatch_async,让 didFinishLaunching 先返回。
    // _tWinMain 内部的消息循环通过 CFRunLoopRunInMode 驱动 main runloop。
    dispatch_async(dispatch_get_main_queue(), ^{
        if (s_iosMain) {
            HINSTANCE hInst = GetModuleHandle(NULL);
            int ret = s_iosMain(hInst, 0, NULL, SW_SHOWNORMAL);
            exit(ret);
        } else {
            exit(-1);
        }
    });
    return YES;
}

对外只暴露一个 C API,应用层纯 C++ 通过 swinx/include/ios_entry.h 调用:

// swinx/include/ios_entry.h
// 应用层 iOS 入口函数原型(与 Win32 WinMain 完全一致)
typedef int (*funIosMain)(HINSTANCE hInstance,
                          HINSTANCE hPrevInstance,
                          LPTSTR    lpstrCmdLine,
                          int       nCmdShow);

extern "C" int swinx_ios_entry(int argc, char *argv[], funIosMain iosMain);

为什么参数与 WinMain 一致

funIosMain 的签名与 Win32 WinMain 完全一致,因此业务侧的 _tWinMain 可以原样复用,无需为 iOS 改写入口逻辑。

2. HWND = SUIView 指针

与其他移动端一样,SOUI 让 HWND 直接承载原生对象指针,避免查表:

// SUIWindow.h
// HWND 直接映射到 SUIView(UIView 子类)的桥接指针。
HWND createUiWindow(HWND hParent, DWORD dwStyle, DWORD dwExStyle,
                    BOOL bAutoDbkClick, LPCSTR pszTitle,
                    int x, int y, int cx, int cy, SConnBase *pListener);
void closeUiWindow(HWND hWnd);
BOOL IsUiWindow(HWND hWnd);
BOOL showUiWindow(HWND hWnd, int nCmdShow);
BOOL invalidateUiWindow(HWND hWnd, LPCRECT rc);
BOOL getUiWindowRect(HWND hWnd, RECT *rc);
Win32 语义 iOS 实现
CreateWindowEx createUiWindow → 创建 SUIView
DestroyWindow closeUiWindow
ShowWindow showUiWindow
MoveWindow / SetWindowPos setUiWindowPos / setUiWindowSize
InvalidateRect invalidateUiWindow
GetWindowRect getUiWindowRect
SetFocus setUiFocusWindow
GetActiveWindow getUiActiveWindow
WindowFromPoint hwndFromPoint

头文件冲突:#undef interface

basetyps.h 中存在 #define interface struct,会与 Objective-C 的 @interface 冲突。混编时需 #undef interfaceios_main.mm 中已处理)。

3. 消息循环:CFRunLoop + 唤醒源

iOS 的消息循环基于 CFRunLoop(替代 macOS 的 NSApplication 事件泵)。由于 UIKit 本身已能分发事件,updateMsgQueue 只需运行 CFRunLoop 即可。

唤醒机制是这里的关键:当工作线程 PostMessage 时,需要唤醒阻塞中的 runloop。

// SConnection.h(节选)
std::list<TimerInfo> m_lstTimer;
void *m_wakeSource;       // CFRunLoopSourceRef
void *m_wakeRunLoop;      // CFRunLoopRef
// SConnection.mm(节选)—— 初始化唤醒源
CFRunLoopSourceContext ctx = {0};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &ctx);
CFRunLoopRef rl = CFRunLoopGetMain();
CFRunLoopAddSource(rl, source, kCFRunLoopDefaultMode);

postMsg 投递消息后调用 stopEventWaiting() 唤醒阻塞的 runloop,从而让 waitMsg 及时返回。

4. 定时器:由 C++ 消息循环驱动(iOS 特有)

这是 iOS 端与其他移动端最显著的差异

  • Android:定时器交给 Java Handler.postDelayed
  • 鸿蒙:定时器交给 ArkTS 的 setTimer
  • iOS:定时器由 C++ 消息循环自行调度,不使用 NSTimer
// SConnection.h
struct TimerInfo
{
    UINT_PTR id;
    HWND     hWnd;
    UINT     elapse;
    UINT     fireRemain;
    TIMERPROC proc;
};

SConnection 维护 std::list<TimerInfo> m_lstTimer,调度逻辑为:

  1. waitMsg 取所有定时器 fireRemain最小值作为 CFRunLoopRunInMode 的超时;
  2. 每次循环按实际流逝时间递减 fireRemain
  3. fireRemain <= elapse 时构造 WM_TIMER 消息投入队列。
// SConnection.mm(节选)
if (!m_bBlockTimer) {
    std::unique_lock<CountMutex> lock(m_mutex);
    for (auto &it : m_lstTimer) {
        timeOut = std::min(timeOut, it.fireRemain);
    }
}
updateMsgQueue(timeOut);

带来的好处

定时器完全在 C++ 侧闭环,不存在跨线程调用 UIKit 的风险(UIKit 要求主线程操作)。对比鸿蒙需要 napi_threadsafe_function 回投,iOS 这条路径更简单。

5. 渲染:Core Graphics

iOS 端渲染后端为 Core Graphics:SOUI 通过 CGContextRef 完成绘制,由 SUIViewdrawRect: 触发,与 macOS 端保持一致的渲染抽象(区别于 Android / 鸿蒙的 Skia 离屏 + 位图上屏)。

6. 剪贴板与输入法

  • 剪贴板SClipboard 基于 UIPasteboard 实现
  • 输入法SImContext / imm.mm / keyboard.mm 处理文本输入与键码映射,键码统一映射回 VK_* 体系

五端统一入口

games/cnchess/client(中国象棋)为例,同一份业务代码 + 同一个 main.cc 覆盖五端:

// games/cnchess/client/main.cc
#if defined(__IOS__)
#include <ios_entry.h>
#endif

// ... _tWinMain 中实现各端共用的窗口逻辑 ...

#if defined(__IOS__)
int main(int argc, char **argv)
{
    return swinx_ios_entry(argc, argv, _tWinMain);
}
#elif !defined(_WIN32) || defined(__MINGW32__)
int main(int argc, char **argv)
{
    HINSTANCE hInst = GetModuleHandle(NULL);
    return _tWinMain(hInst, 0, NULL, SW_SHOWNORMAL);
}
#endif //_WIN32
平台 入口路径
iOS int mainswinx_ios_entry(argc, argv, _tWinMain)
macOS / Linux int main_tWinMain(...)
Windows 原生 WinMain
Android 独立的 android_entry.cc(JNI 入口必须落在桥接模块内)
OHOS 独立的 ohos_entry.cc(N-API 入口必须落在桥接模块内)

为什么只有 Android / 鸿蒙需要独立 entry 文件

JNI 与 N-API 的入口函数必须编译在桥接模块内才能被正确注册,因此这两个平台单独提供 android_entry.cc / ohos_entry.cc。 iOS 因为可以直接从 main 调用 C API,不需要独立的 ios_entry.cc——main.cc 已直接接管。

小差异用条件编译处理即可,例如 iOS 上主窗口默认最大化:

#ifdef __IOS__
dlgMain.ShowWindow(SW_MAXIMIZE);
#else
dlgMain.ShowWindow(SW_SHOWNORMAL);
#endif

环境要求

工具 要求
Xcode 建议最新稳定版
iOS 部署目标 iOS 12+
语言标准 C++11+(Obj-C++ 混编)
构建系统 CMake + Xcode 工程

工程结构

soui4/
├── swinx/
│   ├── include/ios_entry.h         # iOS 入口 C API 声明
│   └── src/platform/ios/           # iOS 平台实现(Objective-C++)
│       ├── ios_main.mm             # AppDelegate + swinx_ios_entry()
│       ├── SConnection.h/.mm       # 消息循环、定时器链表、唤醒源
│       ├── SUIWindow.h/.mm         # 窗口桥接(HWND = SUIView)
│       ├── SClipboard.h/.mm        # 剪贴板(UIPasteboard)
│       ├── SImContext.h            # 输入法
│       ├── keyboard.h/.mm          # 键码映射
│       ├── atoms.h/.mm             # Atom 表
│       ├── imm.mm  ole2.mm  utils.mm
│       ├── dlghelper.mm            # 原生对话框辅助
│       └── STrayIconMgr.h/.mm      # 托盘图标
└── games/cnchess/client/           # 示例:五端共用同一份业务代码
    ├── main.cc                     # iOS / macOS / Linux / Windows 入口
    ├── android_entry.cc            # Android 专用入口
    └── ohos_entry.cc               # OHOS 专用入口

调试与问题排查

常见问题

Q1:界面卡住、触摸无响应

确认 _tWinMain 中的 GetMessage 循环正在运行 CFRunLoop。若业务代码在启动阶段做了阻塞操作(如同步网络请求),会挡住 runloop,导致 UIKit 事件无法分发。原生异步 API(如 UIDocumentPicker)应使用 dispatch_semaphore + CFRunLoopRunInMode 轮询等待(参见 dlghelper.mm)。

Q2:编译报 @interface 相关语法错误

basetyps.h#define interface struct 与 Objective-C 冲突。在混编文件中 #undef interface 即可。

Q3:定时器不准

定时器由 C++ 消息循环调度,精度取决于 CFRunLoopRunInMode 的唤醒时机。若消息队列繁忙或存在长耗时消息处理,定时器会相应延后。避免在消息处理中执行耗时操作。

Q4:后台线程更新 UI 崩溃

UIKit 要求所有 UI 操作在主线程。invalidateUiWindow 等接口应确保在 _tWinMain 所在的消息循环线程调用,或通过 PostMessage 让消息循环代为处理。

Q5:与 macOS 行为不一致

两者共享 Darwin 基础但 UI 框架不同(UIKit vs AppKit)。若出现差异,优先检查 swinx/src/platform/ios 与 macOS 平台层的实现差异。

各平台差异对比

方面 Windows Android iOS OHOS
桥接技术 原生 JNI 无(Obj-C++ 直编) N-API
独立适配库 soui-android-lib soui-ohos-lib
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
SOUI 渲染工厂 Render_Skia / Render_Gdi / Render_D2d Render_Skia Render_Skia Render_Skia
消息循环 原生 GetMessage Handler 调度 CFRunLoop 驱动 ArkTS 调度
定时器 系统 SetTimer Handler.postDelayed C++ 消息循环自驱动 ArkTS setTimer
剪贴板 Win32 API ClipboardManager UIPasteboard 系统剪贴板
入口 WinMain JNI entry swinx_ios_entry N-API entry

文件索引

用途 路径
入口 C API 声明 swinx/include/ios_entry.h
AppDelegate 与入口实现 swinx/src/platform/ios/ios_main.mm
消息循环 / 定时器 swinx/src/platform/ios/SConnection.h / .mm
窗口桥接 swinx/src/platform/ios/SUIWindow.h / .mm
剪贴板 swinx/src/platform/ios/SClipboard.h / .mm
输入法 swinx/src/platform/ios/SImContext.h / imm.mm
键码映射 swinx/src/platform/ios/keyboard.h / .mm
原生对话框 swinx/src/platform/ios/dlghelper.mm
示例(五端共用) games/cnchess/client/main.cc

总结

SOUI 的 iOS 适配体现了"能不架桥就不架桥"的思路:

  1. 零桥接开销:Objective-C++ 直接调用 UIKit,无需 JNI / N-API
  2. 入口极简swinx_ios_entry 一个 C API 即可接入,业务 _tWinMain 原样复用
  3. 消息循环接管CFRunLoop + 唤醒源驱动,与桌面端消息语义一致
  4. 定时器自闭环:由 C++ 消息循环调度,规避跨线程操作 UIKit 的风险
  5. 真正的五端统一:iOS 与桌面共用 main.cc,业务代码零改动

参考 Android 适配指南鸿蒙(OHOS)适配指南 了解其他移动端的实现差异。