news 2026/9/30 18:22:09

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【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 准备交叉编译工具链

# 1. Rust target(按需选 ABI)rustup targetaddaarch64-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 包装器cargoinstallcargo-ndk# 4. 让 cargo-ndk 找到 NDKexportANDROID_NDK_HOME=~/Library/Android/sdk/ndk/<版本号>

ABI 与 target 对照:

ABItarget覆盖设备
arm64-v8aaarch64-linux-android现代手机/平板(主力)
armeabi-v7aarmv7-linux-androideabi老 32 位机(可选)
x86_64x86_64-linux-android模拟器/x86 设备(调试用)
x86i686-linux-android老模拟器(可选)

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

21.2.2 构建 .so

# 在 ffi-bindings crate 目录执行:-o 直接按 ABI 目录结构输出cargondk-tarm64-v8a-tx86_64-o../android-out/jniLibs build--release# 产物形状(Android 期望的 jniLibs 布局):# android-out/jniLibs/# ├── arm64-v8a/libmy_ai_ffi.so# └── x86_64/libmy_ai_ffi.so
# 验证产物确实是"Android 库"而非主机库fileandroid-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 工程

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

接入工程:

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 直接引。

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

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

// 用 20 课的门面模式包住 raw 绑定classAiRepository(dbPath:String,systemPrompt:String){privatevalraw=AiAssistant(dbPath,systemPrompt)// uniffi 生成privatevalscope=CoroutineScope(SupervisorJob()+Dispatchers.Main)funask(sessionId:String,question:String,onText:(String)->Unit,// 回调都切回主线程onError:(UiError)->Unit,){scope.launch{try{valanswer=raw.ask(sessionId,question)// uniffi 映射的挂起函数onText(answer)}catch(e:ApiError){onError(uiError(e))// 20 课话术映射表}}}}

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

@ComposablefunChatScreen(repo:AiRepository,sessionId:String){vartextbyremember{mutableStateOf("")}varquestionbyremember{mutableStateOf("")}Column{Text(text)OutlinedTextField(question,{question=it})Button(onClick={repo.ask(sessionId,question,onText={text+=it},onError={})}){Text("发送")}}}

21.2.5 Android 调试三板斧

# 1. 跨层日志:Rust 侧 println!/tracing 会进 logcatadb logcat|grep-imy_ai_ffi# 2. Rust panic 栈adb shell setprop debug.rust.backtrace1# 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 构建产物

# 必须在 macOS + Xcode。先装 targetrustup targetaddaarch64-apple-ios# 真机rustup targetaddaarch64-apple-ios-sim# Apple Silicon 模拟器rustup targetaddx86_64-apple-ios# Intel 模拟器(可选)# 编静态库(iOS 用 staticlib,链接进 App)cargobuild-pmy_ai_ffi--release--targetaarch64-apple-ioscargobuild-pmy_ai_ffi--release--targetaarch64-apple-ios-sim# 打成 xcframework(真机 + 模拟器两片,Xcode 运行时自动挑)xcrun xcframework create\-librarytarget/aarch64-apple-ios/release/libmy_ai_ffi.a\-librarytarget/aarch64-apple-ios-sim/release/libmy_ai_ffi.a\-outputios-out/MyAiCore.xcframework

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

21.3.2 生成 Swift 绑定并接入 Xcode

uniffi-bindgen generate src/lib.rs\--librarytarget/aarch64-apple-ios/release/libmy_ai_ffi.a\--languageswift --out-dir ios-out/swift# 产物:my_ai_ffi.swift + FFI 头/模块文件

接入两种方式:

方式 A(手动):把 .xcframework 拖进 Target → General → Frameworks; 把生成的 .swift 拖进工程 方式 B(SPM 封装,推荐):建 Swift Package,把 .xcframework 作为 binary target、 Swift 源码作为普通 target → 多 App 复用 + 版本管理更干净
// Swift 壳:把 20 课的 @MainActor 门面接上 raw 绑定importMyAiCore// SPM 包名@MainActorfinalclassAssistantViewModel:ObservableObject{privateletassistant:AiAssistant@Publishedvartext=""@PublishedvarerrorText:String?init()throws{assistant=tryAiAssistant(dbPath:Self.dbPath(),systemPrompt:"你是课程助教")}funcask(question:String){Task{do{// async 绑定在后台跑完,回主线程更新 @Publishedtext+=tryawaitassistant.ask(sessionId:sessionId,question:question)}catchleteasApiError{errorText=Self.friendly(e)// 20 课话术映射}catch{errorText="未知错误"}}}}

最小 SwiftUI 壳:

structContentView:View{@StateObjectprivatevarvm:AssistantViewModel@Stateprivatevarinput=""varbody:someView{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 TargetRust 侧无要求;绑定要求 Swift 版本别低于工程设置
隐私清单网络请求走系统的隐私政策——把涉及网络的说明填进PrivacyInfo.xcprivacy,否则上架审核可能被问
ATS开发期访问 http:// 明文端点需 Info.plist 临时NSAllowsArbitraryLoads;上架前移除,全部走 https
包体积静态库被链接器裁掉未用代码;strip与 release 调优见 22 课

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

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 忘编 staticlibcrate-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、包体积、网络策略、日志上报),最后给出本课程的知识回顾图与结课作业。学完这门课,你会拥有一个"一份核心、多端出包"的完整可交付工程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 18:20:44

Python + pandas 半自动切分Excel数据集:按行数、分组、条件一键拆分

1. 先搞清楚&#xff1a;为什么要做这个半自动化切分工具先说我遇到的实际问题。前阵子帮业务部门整理一份将近两万行的订单明细Excel&#xff0c;领导要求按不同区域拆成独立文件发给各个片区负责人。我第一反应是用透视表加手工筛选&#xff0c;然后复制粘贴。结果弄到第三片…

作者头像 李华
网站建设 2026/9/30 18:20:03

前端核心工具链实操清单:从开发调试到工程化效率提升

不管你是刚入行前端的新人&#xff0c;还是已经带团队的资深开发&#xff0c;应该都有一个感受&#xff1a;前端的技术栈越来越宽&#xff0c;工具链越来越重&#xff0c;但真正能每天派上用场的核心工具&#xff0c;翻来覆去其实就那么几套。这篇文章我想认真聊一份可以“直接…

作者头像 李华
网站建设 2026/9/30 18:17:58

CEPH 块存储实战:从零部署 RBD 到 K8s 接入与性能调优

简介&#xff1a;这份PDF文档面向具备一定存储架构经验的云服务管理人员及开源分布式存储爱好者&#xff0c;系统讲解Ceph块存储的部署与应用。内容以三节点实验集群为背景&#xff0c;在Ubuntu 18.04环境下完成RBD池创建、块设备镜像管理、镜像映射至Linux块设备、格式化挂载及…

作者头像 李华
网站建设 2026/9/30 18:17:52

中英排比句对比:从结构差异到翻译与写作实战技巧

在帮学生改演讲稿、做字幕翻译的那几年&#xff0c;我最常被问到的一个问题是&#xff1a;排比句是不是中国的比英语的更“工整”&#xff1f;很多人的直观印象是&#xff0c;中文排比讲究字数整齐、节奏铿锵&#xff0c;英文排比好像没那么多讲究&#xff0c;这个感受总体没错…

作者头像 李华
网站建设 2026/9/30 18:13:51

从文档生成到可靠交付:基于 Dify 的流程化实践

把一段会议录音整理成一份 Word 文档&#xff0c;听起来像是一个简单的“大模型写作”任务。实际落地后&#xff0c;问题往往不在于模型能否写出一段通顺的话&#xff0c;而在于它能否持续理解不同材料、适配不同模板、在修改后保持一致&#xff0c;并且每次都交付真正可打开的…

作者头像 李华
网站建设 2026/9/30 18:11:22

vscode 扩展Cline、Continue的差别?TaoToken 统一 Key 接入实测对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华