返回列表

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,含义并不完全一样。

平台常见交付物主要关心点
iOSFramework / XCFrameworkSwift/ObjC 接口、架构 slice、符号可见性
AndroidAAR + .soKotlin/Java API、JNI、ABI、manifest 和资源
FlutterPlugin packageDart 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 Framework 的层次边界示意图

什么适合放进 Native Core

我一开始会先看这段逻辑有没有“平台味”。

更适合放进 core更适合留在平台层
请求和响应归一设备 ID
通用模型 schemaKeychain / Keystore
业务错误语义权限弹窗
重试策略支付客户端流程
纯数据转换Flutter widget / Android Activity 生命周期
可用 fake transport 测试的流程具体网络库生命周期和线程回调

判断标准也可以更朴素一点:

  1. 换成 iOS、Android 或 Flutter,这段逻辑本身会不会变?
  2. 它能不能在普通 CMake 测试里跑起来?
  3. 它是不是必须调用某个平台 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
  • 模拟器:arm64x86_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-v8a
  • armeabi-v7a
  • x86_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 往往更稳。

C 和 C++ 在 iOS Android Flutter 中的运行路径示意图

我自己的判断大概是:

场景更合适的方式
纯计算库,输入输出都是基本类型或简单内存块dart:ffi
业务接口以异步 workflow 为主Flutter plugin + MethodChannel
需要访问 Keychain、Keystore、系统设置、购买流程平台 plugin
要嵌入原生 UI 到 FlutterPlatform 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 更适合“配置一次”:

  1. 第一次 configure 创建 native client。
  2. 设置基础环境。
  3. 注入平台能力。
  4. 绑定 session 存储命名空间。
  5. 后续相同配置可以 no-op。
  6. 后续不同配置应该明确失败。

这些值通常不适合在运行中悄悄变化:

  • 后端环境
  • 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
  • 发布链路负责证明不该暴露的实现细节没有漏出去

这类工程的价值,也不是让所有平台代码长得一样。恰恰相反,好的共享层应该允许平台保持自己的写法,只把真正稳定、真正需要一致的业务行为沉下来。