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):
AppDelegate的didFinishLaunching中,用dispatch_async异步调用宿主程序提供的_tWinMain,让didFinishLaunching先返回;- 此时
UIApplication已创建、UIKit 事件系统就绪; _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 interface(ios_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,调度逻辑为:
waitMsg取所有定时器fireRemain的最小值作为CFRunLoopRunInMode的超时;- 每次循环按实际流逝时间递减
fireRemain; - 当
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 完成绘制,由 SUIView 的 drawRect: 触发,与 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 main → swinx_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 适配体现了"能不架桥就不架桥"的思路:
- 零桥接开销:Objective-C++ 直接调用 UIKit,无需 JNI / N-API
- 入口极简:
swinx_ios_entry一个 C API 即可接入,业务_tWinMain原样复用 - 消息循环接管:
CFRunLoop+ 唤醒源驱动,与桌面端消息语义一致 - 定时器自闭环:由 C++ 消息循环调度,规避跨线程操作 UIKit 的风险
- 真正的五端统一:iOS 与桌面共用
main.cc,业务代码零改动
参考 Android 适配指南 与 鸿蒙(OHOS)适配指南 了解其他移动端的实现差异。