4. iOS SDK 目录结构解析归纳总结

刚开始接触 iOS SDK 开发时,我面对打包出来的 .xcframework 文件夹总是一脸懵。里面那么多子目录,到底哪些是必须的?哪些可以删?后来踩了几个坑才摸清楚。今天就把我整理出来的经验分享给你。

SDK 框架的核心骨架

一个标准的 iOS 静态库 SDK(以 .xcframework 为例),展开后大概长这样:

  • Headers/ — 对外公开的头文件
  • PrivateHeaders/ — 私有头文件(一般不开放)
  • Resources/ — 图片、bundle、配置文件
  • Modules/ — 模块映射文件(用于 Swift 兼容)
  • 静态库本体 — 如 .a 或 .framework 里的可执行文件
  • dSYM/ — 符号文件,用于符号化崩溃日志

我把常用目录整理成了下面这个表格,你对照着看会清楚很多:

目录/文件 作用 打包时建议
Headers 暴露给外部调用的 .h 文件 必须保留
PrivateHeaders 内部接口或测试用头文件 建议剔除或隐藏
Resources 图片、Storyboard、xcassets 等资源 按需保留
Modules module.modulemap 和 Swift 接口 支持 Swift 时必须保留
静态库本体 编译后的二进制(.a 或 .framework) 必须保留
dSYM 调试符号文件(回溯崩溃) 强烈建议保留

Headers 与 PrivateHeaders —— 门面与后门

Headers 里放的是你愿意公开给用户的 API。我习惯只把核心功能的 .h 放进去,像内部宏、扩展类这些一律不公开。否则用户一旦误用了私有 API,后续升级很容易出问题。

PrivateHeaders 其实是个「后门」。有些 SDK 会用它导出单元测试用的内部方法,或者给高级用户特殊调用。但我个人建议:正式发布的 .xcframework 最好别带 PrivateHeaders。我曾经就因为这个被人拿去调用私有方法,最后出了问题还被追责,教训深刻。

小技巧: 用 Xcode 的 Copy Files 阶段,只勾选 "Headers",不勾选 "Private Headers"。或者脚本里直接删除 PrivateHeaders 文件夹,干净利落。

Resources 与 Modules —— 资源与兼容性

如果你的 SDK 用到了图片、nib 文件,甚至字体,那 Resources 目录就少不了。有人说直接把这些文件放到主包里不行吗?可以,但容易和宿主 App 的同名资源冲突。所以苹果推荐你打成 .bundle,然后放在 Resources 里。

Modules 目录里藏着两个重要文件:module.modulemap 和 .swiftinterface。前者让 OC 的 #import <Framework/Framework.h> 自动找到头文件;后者让 Swift 调你的 OC 代码时能生成正确的接口签名。说白了,没有这俩文件,你的 SDK 就只能在纯 OC 项目里跑,Swift 项目根本用不了。

注意: 如果你的 SDK 同时支持 CocoaPods 和 SPM,Modules 目录在两种集成方式下的位置可能不同。CocoaPods 下通常会自动生成,但 SPM 需要你在 Package.swift 里手动指定 modulemap 路径。别搞混了。

静态库与 dSYM —— 核心与调试支撑

真正编译出来的二进制就是 静态库。在 .framework 里它叫 FrameworkName(没有后缀),在 .a 里它就是 libxxx.a。你可以用 lipo -info 查看它包含哪些架构。

% lipo -info MySDK.framework/MySDK
Architectures in the fat file: MySDK.framework/MySDK are: armv7 arm64 x86_64

dSYM 文件必须和每个架构一一对应。如果 xcframework 里包含了模拟器和真机的 slice,那 dSYM 应该也有两套。我记得有一次线上崩溃堆栈全是偏移地址,就是因为忘了把 dSYM 上传到 Bugly——你想想看,没符号文件,崩溃根本没法定位,项目差点延期。所以,永远保留 dSYM,即便你用的是 Bitcode。

自定义目录结构的建议

除了标准结构,有些团队会加一些自己的目录:

  • Include/ — 仅放 .h,与二进制分开
  • Lib/ — 多架构的 .a 文件
  • Support/ — 配置文件、脚本等
  • Version/ — 版本号标记文件(便于 CI 自动化检查)

我个人习惯在 .xcframework 顶层加一个 VERSION 文件,里面只写一行版本号。这样 CI 脚本可以快速读取,不需要解压就可以判断版本,速度很快。

下面这张图把 SDK 目录结构的关系画了出来,你看看就明白了:

iOS SDK 目录结构层级图 MySDK.xcframework Headers PrivateHeaders Resources Modules 静态库 + dSYM 公开 .h 文件 内部私有 .h .bundle / 图片 / 字体 modulemap / swiftinterface 二进制 + 符号表 📦 自定义扩展建议 可在顶层加 VERSION 文件、CHANGELOG、LICENSE 等

深入:dSYM 的放置与验证

很多新手把 dSYM 直接扔到 xcframework 的根目录就完事了。其实不对。苹果要求的格式是 dSYMs/ 文件夹,里面放 FrameworkName.framework.dSYM 这个包。如果你用 Xcode 构建 .xcframework 时指定了 -debug-symbols 参数,它会自动生成正确的目录结构。

验证是否缺 dSYM: 在终端执行 dsymutil -o /dev/null - flat MySDK.framework/MySDK。如果没有输出错误,说明 dSYM 可用;否则会提示 "could not find symbols"。遇到这个警告,一定要回头检查 dSYM 路径。

另外我记得有一版 Xcode 13 的 bug:当你同时打多架构的 xcframework 时,dSYM 有时会被重复拷贝。我当时的解决方法是先分别构建每个架构的 framework,再手动用 lipo -create 合并,最后再单独拷贝 dSYM。虽然麻烦,但结果可靠。

总结一下关键点

  1. Headers 要精简——只放要公开的 API,减少用户误用风险。
  2. PrivateHeaders 最好去掉——除非你有充分的理由(比如单元测试私有模块)。
  3. Resources 统一打 bundle——避免资源名冲突。
  4. Modules 文件支持 Swift 调用——没有它,Swift 项目就没法 import 你的 SDK。
  5. dSYM 必须保留——线上崩溃不符号化就等于白崩。
  6. 可以自制 VERSION 文件——对 CI 自动化和版本追溯很有帮助。

理解了目录结构,你就能精准控制 SDK 打包时哪些该留、哪些该删。下一刀下得准,编译时间都能省不少。嗯,今天就聊到这里,后面我们再继续看如何优化编译选项。