在保证 ABI 稳定(Binary Stability)的前提下发布库,意味着你既要让开发者能直接使用你的二进制文件,又要确保你未来对库的修改不会导致现有的 App 崩溃。
这主要依赖于 Swift 的一个核心功能:库进化(Library Evolution) 。
1. 开启编译器的“库进化”开关
要发布一个支持 ABI 稳定的库,首先需要在 Xcode 的 Build Settings 中将 Build Library for Distribution 设置为 YES。
开启该选项后,编译器会发生两个关键变化:
- 生成
.swiftinterface:这是一个文本格式的接口文件。它让不同版本的 Swift 编译器都能理解你的库 API(即模块稳定性)。 - 改变内存布局策略:编译器不再对
struct和class的布局做硬编码假设。
2. 利用 @frozen 和非冻结类型的权衡
在 ABI 稳定的世界里,内存布局就是契约。
非冻结类型(默认行为)
默认情况下,你发布的 struct 或 enum 是非冻结的(Non-frozen) 。
- 灵活性:你可以在未来的版本中给
struct增加新属性,或者给enum增加新case。 - 代价:由于调用方不知道你的类型未来会多大,它不能在栈上固定分配空间,必须通过一个间接层(Indirection)来访问。这会带来微小的性能损耗。
@frozen 属性
如果你确定一个类型的成员永远不会改变(例如 Point(x, y)),可以使用 @frozen。
- 优化:编译器会像编译普通代码一样进行极致优化(如直接在栈上分配,允许内联访问成员)。
- 限制:一旦发布,你永远不能增加、删除或重新排列该类型的成员,否则会破坏 ABI。
3. 使用 @usableFromInline 进行内部优化
如果你想提高性能,通常会使用 @inlinable 将函数体暴露给调用方。但 @inlinable 要求函数内引用的所有东西必须是 public 的,这会破坏封装性。
@usableFromInline 解决了这个问题:
- 它允许你将某些内部实现标记为“对内联可见”,但对外部开发者依然是不可见的(Private/Internal 语义)。
- 场景:你可以优化算法逻辑并标记为
@usableFromInline,然后由一个@inlinable的公共 API 调用它。这样既实现了跨模块内联优化,又保护了你的私有 API。
4. 虚函数表与方法调度
对于 class,ABI 稳定性要求方法调度的顺序保持一致。
- 新增方法:在 ABI 模式下,新增加的方法会被追加到虚函数表(V-table)的末尾。
- 限制:你不能修改已有方法的签名,也不能改变它们的声明顺序,因为调用方是根据“索引”来找函数的。
- 扩展(Extensions) :在
extension中添加的方法使用静态派发或 Obj-C 消息转发,它们不进入 V-table,因此在扩展中添加功能对 ABI 极其友好。
5. 发布工具:XCFramework
最终,你应该将库打包为 .xcframework。这是 Apple 推荐的唯一标准分发格式,它能完美处理:
- 多平台支持(iOS, macOS, 模拟器等)。
- 模块镜像:自动包含
.swiftinterface和二进制符号。 - 库演进:确保你的二进制库在未来的 Swift 环境中依然表现稳健。
总结策略
| 需求 | 做法 | 风险 |
|---|---|---|
| 极致性能 | 使用 @frozen 和 @inlinable | 丧失未来修改布局的灵活性。 |
| 最大灵活性 | 保持默认(非冻结),使用 extension 扩展功能 | 存在间接访问开销,性能略低。 |
| 内部优化 | 结合 @usableFromInline | 内部实现逻辑必须保持二进制兼容。 |