【AI开发之Rust】第 21 课:双端集成与出包 —— Android(.so→AAR)与 iOS(xcframework)

环境:Android 部分 Win/mac/Linux 皆可;iOS 部分必须 macOS + Xcode


21.1 这节课解决什么问题

第 19 课生成了绑定,第 20 课定好了封装模式。本课做最终交付------把同一份 core 打进两个移动平台的"标准包":

复制代码
Android:Rust 编成 .so → 连同 UniFFI 生成的 Kotlin 打成 AAR → Compose 调用
iOS:    Rust 编成静态库 → xcrun 合并成 xcframework → Swift 调用(SwiftUI Demo)

两条管线在"交叉编译"这个点上分道扬镳,但核心资产完全共享:core 的 Rust 代码一个字不改,两端的差异只在"怎么把核心编成对应平台要的形状"。

复制代码
   ┌─────────────── Rust core(一次编写,17-19 课)───────────────┐
   │                                                             │
   ▼                                                             ▼
cargo-ndk + aarch64-linux-android 等              rustup target + aarch64-apple-ios 等
   │ 生成各 ABI 的 .so                                │ 生成真机/模拟器静态库
   ▼                                                 ▼
UniFFI Kotlin 绑定 ──► Android AAR ──► Compose    UniFFI Swift 绑定 + xcframework ──► SwiftUI

💡 本课"重复劳动"偏重(命令多、路径长),目标不是背命令,而是理解每个平台产物的"形状" :Android 要的是 jniLibs/<abi>/libxxx.so,iOS 要的是 xcframework/<平台>/libxxx.a。理解形状后,无论用脚本、Xcode、Gradle 还是 CI,都是把文件放到对的位置。

21.2 Android 线:.so → AAR → Kotlin

21.2.1 准备交叉编译工具链

bash 复制代码
# 1. Rust target(按需选 ABI)
rustup target add aarch64-linux-android armv7-linux-androideabi \
                  x86_64-linux-android i686-linux-android

# 2. NDK:Android Studio SDK Manager 安装,记住路径
#    (命令行 NDK:https://developer.android.com/ndk/downloads)

# 3. cargo-ndk:cargo 的 NDK 包装器
cargo install cargo-ndk

# 4. 让 cargo-ndk 找到 NDK
export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk/<版本号>

ABI 与 target 对照:

ABI target 覆盖设备
arm64-v8a aarch64-linux-android 现代手机/平板(主力)
armeabi-v7a armv7-linux-androideabi 老 32 位机(可选)
x86_64 x86_64-linux-android 模拟器/x86 设备(调试用)
x86 i686-linux-android 老模拟器(可选)

💡 生产通常只发 arm64-v8a +(调试用)x86_64;省包体就别打全四个。

21.2.2 构建 .so

bash 复制代码
# 在 ffi-bindings crate 目录执行:-o 直接按 ABI 目录结构输出
cargo ndk -t arm64-v8a -t x86_64 -o ../android-out/jniLibs build --release

# 产物形状(Android 期望的 jniLibs 布局):
# android-out/jniLibs/
# ├── arm64-v8a/libmy_ai_ffi.so
# └── x86_64/libmy_ai_ffi.so
bash 复制代码
# 验证产物确实是"Android 库"而非主机库
file android-out/jniLibs/arm64-v8a/libmy_ai_ffi.so
# ELF 64-bit LSB shared object, ARM aarch64, for Android NDK ...

⚠️ 一定要 --release(debug 的 .so 巨大且慢);ffi-bindings/Cargo.toml 用 crate-type = ["cdylib", "staticlib"]------Android 走 cdylib(.so),iOS 走 staticlib(.a)。

21.2.3 生成 Kotlin 绑定并接入 Android 工程

bash 复制代码
# 绑定要从"某个架构的产物"里读 ABI 元数据(任选一个已编好的即可)
uniffi-bindgen generate src/lib.rs \
  --library ../target/aarch64-linux-android/release/libmy_ai_ffi.so \
  --language kotlin --out-dir ../android-out/kotlin
# 产物:android-out/kotlin/my_ai_ffi/(含 .kt 绑定与元数据)

接入工程:

bash 复制代码
Android 工程 myapp/
└── app/src/main/
    ├── java/org/example/myapp/...          # 你自己的 Kotlin
    ├── ...(把生成的 my_ai_ffi.kt 放同源集即可)
    └── jniLibs/                          # ★ 把 jniLibs 目录整体拷到这里
        ├── arm64-v8a/libmy_ai_ffi.so
        └── x86_64/libmy_ai_ffi.so

要发 AAR :把这些文件放进一个 Android Library 模块的 src/main/,跑 gradle :lib:assembleRelease 即可得到"含 .so + Kotlin 绑定"的 .aar,其它 App 直接引。

kotlin 复制代码
// Android Library 的 build.gradle.kts 关键项
android {
    defaultConfig {
        ndk { abiFilters += listOf("arm64-v8a", "x86_64") }   // 只打进这两个 ABI
    }
}

21.2.4 Kotlin 壳:协程 + 主线程回 UI

kotlin 复制代码
// 用 20 课的门面模式包住 raw 绑定
class AiRepository(dbPath: String, systemPrompt: String) {
    private val raw = AiAssistant(dbPath, systemPrompt)      // uniffi 生成
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

    fun ask(
        sessionId: String,
        question: String,
        onText: (String) -> Unit,     // 回调都切回主线程
        onError: (UiError) -> Unit,
    ) {
        scope.launch {
            try {
                val answer = raw.ask(sessionId, question)     // uniffi 映射的挂起函数
                onText(answer)
            } catch (e: ApiError) {
                onError(uiError(e))                           // 20 课话术映射表
            }
        }
    }
}

最小 Compose 壳(只做"发命令 + 渲染"):

kotlin 复制代码
@Composable
fun ChatScreen(repo: AiRepository, sessionId: String) {
    var text by remember { mutableStateOf("") }
    var question by remember { mutableStateOf("") }

    Column {
        Text(text)
        OutlinedTextField(question, { question = it })
        Button(onClick = {
            repo.ask(sessionId, question, onText = { text += it }, onError = {})
        }) { Text("发送") }
    }
}

21.2.5 Android 调试三板斧

bash 复制代码
# 1. 跨层日志:Rust 侧 println!/tracing 会进 logcat
adb logcat | grep -i my_ai_ffi
# 2. Rust panic 栈
adb shell setprop debug.rust.backtrace 1
# 3. .so 找不到:UnsatisfiedLinkError → 检查 jniLibs 目录名/ABI 拼写/abiFilters

21.2.6 Android 发布注意

事项 说明
minify/R8 绑定是普通 Kotlin,默认规则即可;混淆后报 UnsatisfiedLinkError 就加 -keep class my_ai_ffi.** { *; }
ABI 分包 abiFilters 只留 arm64-v8a 减小包体(Play 还支持 AAB 自动分发)
主线程纪律 同步重活别放主线程;async 方法已在线程池跑
release 调优 见 22 课 [profile.release] + strip

21.3 iOS 线:xcframework → Swift

21.3.1 构建产物

bash 复制代码
# 必须在 macOS + Xcode。先装 target
rustup target add aarch64-apple-ios             # 真机
rustup target add aarch64-apple-ios-sim         # Apple Silicon 模拟器
rustup target add x86_64-apple-ios              # Intel 模拟器(可选)

# 编静态库(iOS 用 staticlib,链接进 App)
cargo build -p my_ai_ffi --release --target aarch64-apple-ios
cargo build -p my_ai_ffi --release --target aarch64-apple-ios-sim

# 打成 xcframework(真机 + 模拟器两片,Xcode 运行时自动挑)
xcrun xcframework create \
  -library target/aarch64-apple-ios/release/libmy_ai_ffi.a \
  -library target/aarch64-apple-ios-sim/release/libmy_ai_ffi.a \
  -output ios-out/MyAiCore.xcframework

⚠️ 现代标准是 xcframework (可同时含真机+模拟器片),lipo 合并多架构属于旧做法;Windows 编不了 iOS,CI 用 macOS runner(22 课给 workflow)。

21.3.2 生成 Swift 绑定并接入 Xcode

bash 复制代码
uniffi-bindgen generate src/lib.rs \
  --library target/aarch64-apple-ios/release/libmy_ai_ffi.a \
  --language swift --out-dir ios-out/swift
# 产物:my_ai_ffi.swift + FFI 头/模块文件

接入两种方式:

css 复制代码
方式 A(手动):把 .xcframework 拖进 Target → General → Frameworks;
               把生成的 .swift 拖进工程
方式 B(SPM 封装,推荐):建 Swift Package,把 .xcframework 作为 binary target、
               Swift 源码作为普通 target → 多 App 复用 + 版本管理更干净
swift 复制代码
// Swift 壳:把 20 课的 @MainActor 门面接上 raw 绑定
import MyAiCore                       // SPM 包名

@MainActor
final class AssistantViewModel: ObservableObject {
    private let assistant: AiAssistant
    @Published var text = ""
    @Published var errorText: String?

    init() throws {
        assistant = try AiAssistant(
            dbPath: Self.dbPath(), systemPrompt: "你是课程助教")
    }

    func ask(question: String) {
        Task {
            do {
                // async 绑定在后台跑完,回主线程更新 @Published
                text += try await assistant.ask(
                    sessionId: sessionId, question: question)
            } catch let e as ApiError {
                errorText = Self.friendly(e)     // 20 课话术映射
            } catch { errorText = "未知错误" }
        }
    }
}

最小 SwiftUI 壳:

swift 复制代码
struct ContentView: View {
    @StateObject private var vm: AssistantViewModel
    @State private var input = ""

    var body: some View {
        VStack {
            ScrollView { Text(vm.text).frame(maxWidth: .infinity, alignment: .leading) }
            HStack {
                TextField("问点什么", text: $input)
                Button("发送") { vm.ask(question: input); input = "" }
            }
        }
        .padding()
    }
}

21.3.3 iOS 调试与注意

事项 说明
模拟器 vs 真机 模拟器跑在真机片会报 "built for iOS Simulator but linking ... built for iOS";缺 sim 片就加 sim target 再编一次
签名 xcframework 不签名,Xcode 对 App 统一签名即可
Deployment Target Rust 侧无要求;绑定要求 Swift 版本别低于工程设置
隐私清单 网络请求走系统的隐私政策------把涉及网络的说明填进 PrivacyInfo.xcprivacy,否则上架审核可能被问
ATS 开发期访问 http:// 明文端点需 Info.plist 临时 NSAllowsArbitraryLoads;上架前移除,全部走 https
包体积 静态库被链接器裁掉未用代码;strip 与 release 调优见 22 课

21.4 双端联调套路(推荐顺序)

markdown 复制代码
1. core: cargo test(不碰任何平台)
2. ffi-bindings: cargo build --release(主机) + Python 冒烟(19 课通道)
3. Android: cargo-ndk 出 so → logcat 里看 Hello/Rust 日志 → 再跑真实用例
4. iOS:    xcframework → 先模拟器跑通 → 再真机
5. 双端各跑:登录/会话历史(本地库) → 流式问答 → 断网降级 → 重启后历史还在

💡 每端第一次接通的"最小冒烟"别接 UI:先在 Android/iOS 里只调 listSessions()/listSessions()(不联网),确认"库能加载、绑定能跑",再逐步往上叠。定位问题时永远先砍到最小可复现。

21.5 常见坑清单(跨端)

症状 原因 对策
UnsatisfiedLinkError / "dylib not found" jniLibs 路径、ABI 目录拼错;cdylib 编了但没拷 核对 jniLibs/<abi>/lib*.so
iOS 链接一堆 undefined symbol 编了 cdylib 忘编 staticlib crate-type 含 "staticlib"
模拟器/真机互相不认 target 片不对 检查 .a 的 file/arch
换 NDK 版本后编译炸 工具链与 NDK 不匹配 固定 NDK 版本并记录;CI 与本地一致
绑定与库版本不一致 bindgen 版本 ≠ crate 版本 cargo install uniffi_bindgen --version 对齐
release 下行为不同 优化导致的时序/溢出被掩盖或放大 debug/release 各跑一遍核心测试

21.6 📝 动手练习

  1. Android 出包 :装 target + cargo-ndk,编出 arm64-v8a 的 .so,用 file/readelf 验证架构;把 19 课的 Python 冒烟换成"Android 工程里调 listSessions()"。
  2. AAR 演示 :建一个 Android Library 模块收 jniLibs/ + 绑定 Kotlin,assembleRelease 后解包 AAR 确认 .so 与类都在。
  3. Compose 壳:实现 21.2.4 的 ChatScreen,能发问并展示本地历史(不接真实 LLM 也可用 core 的 Echo/Fake 客户端)。
  4. iOS 出包:macOS 上编真机+simulator 两个静态库并打成 xcframework;用 SPM 封装接入一个空 SwiftUI 工程。
  5. iOS 冒烟 :SwiftUI 里调 listSessions 创建会话并打印 id;再跑通一次流式问答(LlmSession 轮询版)。
  6. 断网演练:双端各验一次 20 课的离线策略(历史可看、发送降级、重试可用)。
  7. 记录手册 :把两条出包命令 + NDK/Xcode 版本 + 本机踩的坑写成 docs/build-notes.md(22 课的"可复现构建"基础)。

验收门禁:能不看笔记说清 Android 与 iOS 两个产物的形状与各自要求;能说出模拟器片缺失时的报错长什么样;能在任意一端独立完成"编库 → 打包装 → 最小调用"。

✅ 本节小结

  • 两种产物 :Android 要 jniLibs/<abi>/lib*.so(cdylib);iOS 要 xcframework 里嵌 .a(staticlib);
  • Android :cargo ndk -t ... -o jniLibs build --release → 拷目录/打 AAR → Kotlin 挂起函数 + Compose 壳;logcat + backtrace 调跨层问题;
  • iOS :rustup target add → 多 target 编 .a → xcrun xcframework create → SPM/手动接入 → @MainActor 壳 + SwiftUI;
  • 纪律 :release 构建、abiFilters/target 按需、主线程不回传重活、ATS/隐私清单上架前核对;
  • 联调顺序:core 测试 → Python 冒烟 → 单端最小冒烟 → 真实用例 → 断网演练;
  • 不变资产:core 零改动,两条管线只负责"编成平台要的形状"。

下一课预告 :第 22 课《一键多平台与工程收尾》------把两条手工管线固化成 scripts/build-all.sh 与 CI 矩阵、补上桌面/Web 的快速扩展、发布前 checklist(release 优化、strip、包体积、网络策略、日志上报),最后给出本课程的知识回顾图与结课作业。学完这门课,你会拥有一个"一份核心、多端出包"的完整可交付工程。

相关推荐
Amos_Web1 小时前
Rspack 源码解析(十六):多类型资源如何进入 Compilation.assets
前端·rust·前端框架
Norna1 小时前
时间轮里那行凭空出现的 +1
rust
隔窗听雨眠5 小时前
FunProxy用Rust构建跨平台全链路测试抓包代理工具
开发语言·后端·rust
geovindu5 小时前
rust: search
开发语言·后端·rust
孙启超6 小时前
【AI开发之Rust】第 20 课:壳侧 API 封装 —— 把 Rust 能力接进真实 UI
开发语言·后端·rust
柯南46681 天前
【AI开发之Rust】第 20 课:壳侧 API 封装 —— 把 Rust 能力接进真实 UI
rust·编程语言
Yunovian1 天前
AI时代,针对模型与应用,浅谈一下各编程语言
开发语言·c++·人工智能·python·ai·rust·ai编程
Kapaseker1 天前
Rust 凭什么不用垃圾回收?先搞懂 Owner
rust