9-15.【编译与优化】如何在保证 ABI 稳定的前提下发布库,同时允许内部实现优化或扩展?

1 阅读3分钟

在保证 ABI 稳定(Binary Stability)的前提下发布库,意味着你既要让开发者能直接使用你的二进制文件,又要确保你未来对库的修改不会导致现有的 App 崩溃。

这主要依赖于 Swift 的一个核心功能:库进化(Library Evolution)


1. 开启编译器的“库进化”开关

要发布一个支持 ABI 稳定的库,首先需要在 Xcode 的 Build Settings 中将 Build Library for Distribution 设置为 YES

开启该选项后,编译器会发生两个关键变化:

  1. 生成 .swiftinterface:这是一个文本格式的接口文件。它让不同版本的 Swift 编译器都能理解你的库 API(即模块稳定性)。
  2. 改变内存布局策略:编译器不再对 structclass 的布局做硬编码假设。

2. 利用 @frozen 和非冻结类型的权衡

在 ABI 稳定的世界里,内存布局就是契约。

非冻结类型(默认行为)

默认情况下,你发布的 structenum非冻结的(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内部实现逻辑必须保持二进制兼容。