2026年6月29日 · 约 11 分钟读完
移动端跨平台 Native Framework 的设计边界
从 iOS、Android 和 Flutter 的集成方式出发,整理把 C/C++ 业务核心包装成多端 SDK 时需要考虑的边界、运行方式和发布约束。
背景
最近做过一类内部能力层:底层是一份 C/C++ 实现,上层分别包装给 iOS、Android 和 Flutter 用。
它不负责 UI,也不关心具体页面怎么写,主要处理这些相对稳定的内容:
- 业务 workflow
- 模型归一
- 账号/session
- 上传
- 权益校验
- 错误语义
这类东西很容易被叫成“跨平台 Framework”或者“Native Core”。名字其实不重要,真正需要想清楚的是边界。
我这里会刻意避开具体项目名、接口名和业务字段,只记录这类工程本身的思路。因为一旦把文章写成某个 SDK 的 README,读者容易被细节带偏;而这件事里真正可复用的,反而是边界划分。
这篇文章主要回答几个问题:
- C/C++ core 适合放什么?
- iOS、Android、Flutter 分别怎么调用 native code?
- 为什么 C ABI 常常比 C++ class 更适合作为跨语言边界?
- Flutter 什么时候走 MethodChannel,什么时候才适合 FFI?
- 一个 Framework 发布出去前,到底应该检查什么?
Framework 不只是一个包
不同平台说 Framework,含义并不完全一样。
| 平台 | 常见交付物 | 主要关心点 |
|---|---|---|
| iOS | Framework / XCFramework | Swift/ObjC 接口、架构 slice、符号可见性 |
| Android | AAR + .so | Kotlin/Java API、JNI、ABI、manifest 和资源 |
| Flutter | Plugin package | Dart API、MethodChannel、iOS/Android 原生产物 |
Apple 平台上的 Framework 是很具体的东西:目录结构、Mach-O binary、Headers、Modules、Swift interface、Info.plist,都有一套自己的规则。现在要同时支持真机和模拟器,通常还会再包成 XCFramework。
Android 这边一般不会把业务 SDK 叫 Framework,更常见的是 AAR。AAR 里可以放 Kotlin/Java 代码、manifest、资源,也可以放按 ABI 分开的 .so。
Flutter plugin 又再包了一层。Dart 侧看到的是 package API,iOS 和 Android 侧各自有平台实现。这个 plugin 里面还可以继续带 iOS 的 XCFramework 和 Android 的 native library。
所以我现在更倾向于把 Framework 理解成一个交付边界,而不是某一种文件格式。它至少要回答:
- 调用方应该看到什么 API?
- 平台 adapter 要承担什么职责?
- native core 暴露到什么程度?
- release 包里哪些符号和字符串应该被隐藏?
如果调用方还要自己解析底层 JSON、自己管理 C 字符串、自己判断错误码,那复杂度并没有消失,只是被推给了集成方。

什么适合放进 Native Core
我一开始会先看这段逻辑有没有“平台味”。
| 更适合放进 core | 更适合留在平台层 |
|---|---|
| 请求和响应归一 | 设备 ID |
| 通用模型 schema | Keychain / Keystore |
| 业务错误语义 | 权限弹窗 |
| 重试策略 | 支付客户端流程 |
| 纯数据转换 | Flutter widget / Android Activity 生命周期 |
| 可用 fake transport 测试的流程 | 具体网络库生命周期和线程回调 |
判断标准也可以更朴素一点:
- 换成 iOS、Android 或 Flutter,这段逻辑本身会不会变?
- 它能不能在普通 CMake 测试里跑起来?
- 它是不是必须调用某个平台 SDK?
如果答案是“不会变、能独立测试、不依赖平台 SDK”,那它比较适合进入 core。
反过来,如果一段逻辑需要权限弹窗、系统存储、购买流程、Flutter engine 或 Android lifecycle,那它就应该留在平台层。
我觉得 native core 最好的状态,是它可以在没有真实网络、没有模拟器、没有 Flutter engine 的情况下跑测试。HTTP 可以用 fake transport,平台设备信息可以由 adapter 注入。
这里有个常见误区:觉得 C++ 层越厚,复用越充分。实际项目里经常相反。core 越厚,平台层越薄,看起来很省事;但等到 iOS 和 Android 的生命周期、网络、支付、存储细节开始分叉时,这个厚 core 就会变成最难改的地方。
core 只负责稳定业务语义。平台怎么运行,是平台层自己的事。
iOS 上的运行方式
编译和分发
iOS 上的 C/C++ 代码会被 AppleClang 编译成 Mach-O 目标文件,然后和 App 或 Framework 一起链接。
常见架构大概是:
- 真机:
arm64 - 模拟器:
arm64或x86_64 - 对外分发:通常用 XCFramework 包住不同 slice
调用路径
一个比较常见的调用路径是:
Swift API
-> Objective-C / C bridge
-> C ABI
-> C++ core
-> 平台能力注入
Swift 层应该尽量像一个正常的 Swift SDK:
- async/await
- Swift struct
- Swift enum
throws
App 侧不应该直接碰到底层 C 指针,也不应该关心某个 response 里的字符串什么时候失效。
为什么不用 C++ class 直接暴露
我会避免直接暴露 C++ class。C++ ABI 受很多因素影响:
- 编译器
- 标准库
- 编译选项
- 异常模型
- name mangling
拿它做跨语言公开边界,后面会很难控制兼容性。
相比之下,C ABI 虽然朴素,但更适合做这层边界。函数、结构体、整数、字符串指针,表达能力不花哨,反而更稳定。
抽象出来大概是这种感觉:
typedef struct native_response {
int ok;
int error_code;
int platform_status;
int business_code;
const char* message;
const char* body;
} native_response;
int native_load_resource(
native_client* client,
native_response* response
);
真实的 SDK 不应该把这层直接丢给 App 开发者。Swift adapter 要把它翻译成平台模型和平台错误。C ABI 是 adapter 和 core 之间的合同,不是最终产品 API。
Android 上的运行方式
native library 和 ABI
Android 的 native code 通过 NDK 编译成 .so。每个 CPU ABI 都是独立产物,不能互相替代。
常见 ABI 包括:
arm64-v8aarmeabi-v7ax86_64
很多时候真机只测 arm64,问题不明显;到了 x86_64 模拟器,才发现包里根本没有对应 .so。如果这个 SDK 要给别人集成,支持哪些 ABI 必须写清楚,不能靠调用方猜。
调用路径
Android 侧的调用链通常是:
Kotlin API
-> JNI
-> C ABI
-> C++ core
-> 平台能力注入
Kotlin 里用 System.loadLibrary 加载 native library,再通过 external fun 进入 JNI。JNI 负责把 Java/Kotlin 的对象、字符串、数组转换成 C 边界能处理的数据。
JNI 这层不要低估
JNI 看起来只是一层桥,真正调试起来,会牵涉很多细节:
- native 线程回调 JVM 时,要拿到正确的
JNIEnv - 必要时要 attach 当前线程
- 跨调用保存 Java 对象,不能用局部引用凑合
- 从 Java 字符串拿到的字符指针,不能想当然长期持有
- native exception 和 Java exception 要有明确处理策略
另外,native code 仍然在 App 进程里跑。它不是服务端,也不是沙箱外的安全区域。非法指针、内存越界、线程竞争,一样会让 App 崩。
Flutter 先不要急着上 FFI
Flutter 调 C/C++,第一反应很容易是 dart:ffi。它确实很适合一类场景:
- 纯计算
- 少平台依赖
- 输入输出简单
- 不需要复杂生命周期
比如图像处理、音频处理、压缩、加密、算法模块,都比较自然。
但业务 SDK 往往不是这种形态。它可能要拿设备信息,要读平台存储,要处理文件路径,要走平台网络,要和购买流程交互,还要把结果回到 Flutter 主线程。这时候直接让 Dart FFI 去碰所有细节,并不会让工程更简单。
Platform Channel 在这里反而更像一个合理的边界:
- Dart 侧保持稳定的 Future API 和 model
- iOS/Android 各自处理平台运行时
- native core 只负责真正能复用的业务语义
一个 Flutter plugin 可能会长这样:
Dart API
-> MethodChannel
-> iOS Swift plugin / Android Kotlin plugin
-> C ABI 或平台 SDK
-> C++ core
这不是说 FFI 不好,而是要看它解决的是哪类问题。如果核心能力就是一个纯 native library,FFI 很直接;如果能力本身强依赖平台上下文,MethodChannel 往往更稳。

我自己的判断大概是:
| 场景 | 更合适的方式 |
|---|---|
| 纯计算库,输入输出都是基本类型或简单内存块 | dart:ffi |
| 业务接口以异步 workflow 为主 | Flutter plugin + MethodChannel |
| 需要访问 Keychain、Keystore、系统设置、购买流程 | 平台 plugin |
| 要嵌入原生 UI 到 Flutter | Platform View |
| iOS 原生 App 集成 | Swift SDK |
| Android 原生 App 集成 | Kotlin SDK / AAR |
这个表不是规则,只是一个起点。真正要看的还是边界是不是自然。
C ABI 要写清楚内存和线程
跨语言 SDK 里,C ABI 经常是最稳定、也最容易被误用的一层。
我比较反对把 C API 直接当产品 API 暴露出去。让业务开发者处理 const char*、response 生命周期、payload JSON 和 error code,通常不是降低复杂度,而是把 SDK 作者应该处理的问题转嫁给调用方。
C ABI 更适合做 adapter 边界。Swift、Kotlin、Dart 的平台层往下调用它,再把结果翻译成各自语言里自然的类型。
这层至少要写清楚:
| 约定 | 需要明确的问题 |
|---|---|
| 字符串所有权 | 哪些由 client 持有,哪些需要调用方释放 |
| response 生命周期 | 指针有效期到什么时候 |
| 并发模型 | 同一个 client 能不能并发调用 |
| 销毁行为 | client 销毁后哪些指针全部失效 |
| 回调线程 | 结果在哪个线程返回,平台层是否需要切主线程 |
这些约定看起来琐碎,但它们决定了这个 SDK 后面是不是可维护。跨 Swift、Kotlin、Dart、C++ 的边界上,内存所有权不能靠默契。
如果底层 client 没有设计成可并发 workflow,平台层就应该串行调用,或者为不同会话创建不同 client。不要把并发压力偷偷推给一个没有同步语义的 native 对象。
平台能力用注入,不要写死
我更喜欢让 core 构造抽象请求,再由平台层真正发送请求。
core 可以描述:
- method
- url
- headers
- query
- body
- multipart files
- timeout
但它不应该直接绑定某个具体网络库。
这样做有几个现实好处:
- core 不需要直接依赖 URLSession、OkHttp、Dart http 或其他平台库
- 平台层可以自己处理代理、证书、任务取消、后台线程和主线程回调
- 文件路径、系统语言、设备标识这些平台信息可以由 adapter 注入
- native core 可以用 fake transport 做测试
我觉得这条边界很重要:
native core 应该稳定,但不应该假装自己就是平台。
配置和生命周期
一个 SDK 如果允许在任何时候用不同参数重新配置,后面大概率会出问题。
这类 framework 更适合“配置一次”:
- 第一次 configure 创建 native client。
- 设置基础环境。
- 注入平台能力。
- 绑定 session 存储命名空间。
- 后续相同配置可以 no-op。
- 后续不同配置应该明确失败。
这些值通常不适合在运行中悄悄变化:
- 后端环境
- app identity
- 接口路径映射
- session 存储命名空间
- native client 指针
- 当前设备信息和当前 session
如果 App 真的需要切换环境,应该显式销毁旧实例、创建新实例,或者重启进程。不要在一个已经跑过 workflow 的 native client 上直接换底座。
发布包要检查什么
这类 SDK 的发布链路,不应该只看“能不能编译”。能编译只是第一步,真正要验证的是发布包暴露了什么、隐藏了什么。
一个大致的发布形态可能是:
C / C++ build
-> 生成 native core 和 C ABI library
Apple package
-> 生成 Framework / XCFramework
-> 暴露 Swift / ObjC public surface
-> 隐藏内部 C / C++ 符号
Android package
-> 生成 AAR
-> 携带目标 ABI 的 native .so
-> 让 Kotlin / Java bridge 加载 native library
Flutter package
-> 暴露 Dart API
-> 携带 iOS / Android 原生产物
-> 统一 MethodChannel 语义和错误模型
发布前我会至少看这些东西:
| 检查项 | 为什么要看 |
|---|---|
| 公开符号 | 确认只暴露预期 API |
| C++ implementation symbols | 防止内部实现漏出去 |
| 测试地址、默认 path、内部字符串 | 防止 release artifact 带出内部信息 |
| Swift/Dart/Kotlin public API | 判断是否破坏兼容性 |
| AAR ABI | 确认目标设备和模拟器能加载 |
| XCFramework slice | 确认真机和模拟器都可用 |
| 示例和 smoke test | 确认集成路径能跑通 |
| C API 内存和线程约定 | 确认底层合同没有变 |
native code 不会自动带来安全感。真正有用的是缩小导出面、明确配置注入、检查符号和字符串、保留 smoke test。
几个容易踩的坑
把 C++ 当成跨平台万能层
C++ 可以复用业务逻辑,但不能抹掉平台差异。
权限、存储、支付、线程、网络、生命周期都还是平台问题。core 如果试图吞掉所有差异,最后会变成一个没有平台手感、也没有足够隔离性的中间层。
直接把 C API 暴露给 App
C API 适合做 adapter 边界,不适合做普通 App 开发接口。
业务开发者应该拿到 Swift、Kotlin 或 Dart 里自然的模型和错误,而不是一堆底层指针和字符串生命周期说明。
忽略 ABI 和架构切片
iOS 真机、iOS 模拟器、Android arm64、Android x86_64 都是不同产物。
Flutter plugin 能不能跑,不只取决于 Dart 代码,也取决于底层 native artifacts 是否齐全。
只验证 debug
native SDK 的很多问题只在 release 暴露:
- 符号可见性
- strip
- 字符串残留
- path 注入
- Android minify
- Swift interface
- AAR 内容
- XCFramework slice
debug 示例能跑,只能说明开发环境下的路径通了。
没有写清楚内存和线程约定
跨语言边界上,最危险的不是少写一个方法,而是所有权不清楚。
谁创建、谁释放、指针有效期到什么时候、能不能并发调用、回调在哪个线程返回,都应该进入文档和测试。
总结
做 iOS、Android、Flutter 共享 Native Framework,本质上不是把 C++ 编译到三个平台。编译只是最容易看见的部分。
更难的是边界:
- C/C++ core 负责稳定业务语义
- C ABI 负责底层调用合同
- Swift、Kotlin 和 Dart 负责把 native 能力翻译成各自平台自然的 API
- 发布链路负责证明不该暴露的实现细节没有漏出去
这类工程的价值,也不是让所有平台代码长得一样。恰恰相反,好的共享层应该允许平台保持自己的写法,只把真正稳定、真正需要一致的业务行为沉下来。