首页 Swift ABI 稳定后的几个问题
文章
取消

Swift ABI 稳定后的几个问题

ABI 的稳定,会让 Swift 在 Apple 平台上有更大的发展。不过由于历史原因,在系统支持和兼容性方面,我们还会面临一些问题。Swift 官方博客也专门发了一篇文章 Evolving Swift On Apple Platforms After ABI Stability 来进行阐述,来看看他们是怎么说的。

随着 Swift 5 的发布,Swift 的 ABI 也终于稳定下来,并作为 macOS、iOS、tvOS 和 watchOS 核心组件提供。ABI 的稳定所带来的好处很多,最明显的就是 Swift 写的应用不再需要带着 Swift 运行时库一起分发,大大减小了应用的体积。操作系统层面也能更好地集成和优化 Swift 运行时,从而让 Swift 程序启动更快,获得更好的运行时性能,同时降低内存的消耗。而且 Apple 也可以在未来版本的系统中使用 Swift 来提供平台级的 framework。当后续版本的 Swift 能提供模块稳定性时,第三方也可以发布 Swift 编写的二进制 framework。

不过也正是由于 ABI 的稳定,Swift 不再是开发人员工具链的一部分。因此,后续采用新的 Swift 运行时和标准库功能的项目,可能需要新版本的系统。其实 Objective-C 也有这个问题,需要在新的语言特性/框架和老版本系统之间做权衡。

什么是 ABI 稳定

就是 binary 接口稳定,也就是在运行的时候只要是用 Swift 5 (或以上) 的编译器编译出来的 binary,就可以跑在任意的 Swift 5 (或以上) 的 runtime 上。这样,我们就不需要像以往那样在 app 里放一个 Swift runtime 了,Apple 会把它弄到 iOS 和 macOS 系统里。

APP 尺寸会变小?

是的,但是这是 Apple 通过 App Thinning 帮我们完成的,不需要你操心。在提交 app 时,Apple 将会按照 iOS 系统创建不同的下载包。对于 iOS 12.2 的系统,因为它们预装了 Swift 5 的 runtime,所以不再需要 Swift 的库,它们会被从 app bundle 中删掉。对于 iOS 12.2 以下的系统,外甥打灯笼,照旧。

一个新创建的空 app,针对 iOS 12.2 打包出来压缩后的下载大小是 26KB而对 iOS 12.0 则是 2.4MB。如果你使用了很多标准库里的东西,那这个差距会更大 (因为没有用到的标准库的符号会被 strip 掉),对于一个比较有规模的 app 来说,一般可以减小 10M 左右的体积。

ABI稳定好处

因为系统集成了 Swift,所以大家都用同一个 Swift 了,app 启动的时候也就不需要额外加载 Swift,所以在新系统上会更快更省内存。当然啦,只是针对新系统。

另外,对于 Apple 的工程师来说,他们终于能在系统的框架里使用 Swift 了。这样一来,很多东西就不必通过 Objective-C wrap 一遍,这会让代码运行效率提高很多。虽然在 iOS 12.2 中应该还没有 Swift 编写的框架,但是我们也许能在不久的将来看到 Swift 被 Apple 自己所使用。等今年 WWDC 的消息吧。

我还想用一段时间的 Xcode 10.1,不太想这么快升级

Xcode 10.1 里的是 Swift 4.2 的编译器,出来的 binary 不是 ABI 稳定的,而且必定打包了 Swift runtime。新的系统发现 app 包中有 Swift runtime 后,就会选择不去使用系统本身的 Swift runtime。这种情况下一切保持和现在不变。旧版本的 Xcode 只有旧版本的 iOS SDK,所以自然你也没有办法用到新系统的 Swift 写的框架,系统肯定不需要在同一个进程中跑两个 Swift runtime。

简单说,你还可以一直使用 Xcode 10.1 直到 Apple 不再接受它打包的 app。不过这样的话,你不能使用新版本 Swift 的任何特性,也不能从 ABI 稳定中获得任何好处。

我升级了 Xcode 10.2,但是还想用 Swift 4 的兼容模式,会怎么样?

首先你需要弄清楚 Swift 的编译器版本语言兼容版本的区别:

编译器版本语言兼容版本对应的 Xcode 版本
Swift 5.0Swift 5.0, 4.2, 4.0Xcode 10.2
Swift 4.2Swift 4.2, 4.0, 3.0Xcode 10.0, Xcode 10.1

同一个 Xcode 版本默认使用的编译器版本只有一个 (在你不更换 toolchain 的前提下),当我们在说到“使用 Xcode10.2 的 Swift 4 兼容模式”时,我们其实指的是,使用 Xcode 10.2 搭载的 Swift 5.0 版本的编译器,它提供了 4.2 的语法兼容,可以让我们不加修改地编译 Swift 4.2 的代码。即使你在 Xcode 10.2 中选择语言为 Swift 4,你所得到的二进制依然是 ABI 稳定的。ABI 和你的语言是 Swift 4 还是 Swift 5 无关,只和你的编译器版本,或者说 Xcode 版本有关。

:::tip

多提一句,即使你选择了 Swift 4 的语言兼容,只要编译器版本 (当然,以及对应的标准库版本) 是 5.0 以上,你依然可以使用 Swift 5 的语法特性 (比如新增加的类型等)。

:::

Xcode 10.2 编译

我们是否必须使用 Xcode 10.2 重新编译现有的 Swift 应用程序才能在最新的操作系统上运行?

捆绑了 Swift 运行时库的现有 Swift 二进制文件将可以继续在 macOS 10.14.4、iOS 12.2、tvOS 12.2、watchOS 5.2 和未来的 OS 版本上运行。这些应用程序将继续使用捆绑的 Swift 运行时运行,因为这些较旧的 Swift 运行时与稳定的 Swift ABI 不兼容。操作系统中的 Swift 运行时在设计时是与捆绑的 Swift 运行时隔离的,两者都视对方为普通的 Objective-C 类。不过使用捆绑运行时的应用程序无法获得 App Store app thinning 的优势。

Swift运行时

我可以选择将新的 Swift 运行时与我的应用程序捆绑在一起,以便能够使用新的运行时功能而无需新的操作系统吗?

由于一些原因,这一点是不支持的:

  • 用于保持与先前稳定版本的 Swift 运行时兼容的共存功能依赖于在单个进程中只有一个 Swift 运行时,并且使用先前稳定版本的 Swift 运行时的代码都是自包含运行时作为应用的一部分。如果允许捆绑新的 Swift 运行时,并与 OS Swift 运行时一起运行,那么新的运行时将无法访问系统中的 Swift 库或与 OS 运行时链接的 ABI 稳定的第三方 Swift 库。
  • 使用捆绑的运行时替换 OS 运行时,将直接规避系统库的安全性,系统库基于其所使用的运行时的操作系统版本进行代码签名。
  • 此外,如果可以替换 OS Swift 运行时,这会导致 OS、Swift运行时、第三方库和应用程序进行测试时,都必须为其配置矩阵添加一个维度。像这样的“DLL地狱”情况使得测试,鉴定和交付代码变得更加困难和昂贵。
  • 将 Swift 运行时库集成在 OS 中,可使其与 OS 的其他组件紧密集成,特别是 Objective-C 运行时和 Foundation 框架。OS 运行时库也可以合并到 dyld 共享缓存中,这样与共享缓存外的 dylib 相比,它们具有最小的内存和加载时间开销。在 OS 之外构建的运行时可能无法完全复制 OS 运行时的行为,或者在限制使用稳定的 API 时这样做可能会带来显着的性能成本。

语言的兼容性

什么类型的语言功能和演进提议可能仅限于未来的操作系统版本?

任何需要新的 Swift 运行时或标准库支持的功能都可能受操作系统可用性限制。这包括:

  • 标准库的附加内容,包括新类型、协议、协议一致性、函数、方法或属性。
  • 对 Swift 类型系统的更改,例如新类型、现有类型的新修饰符(例如函数类型属性)、新桥接、子类型和/或动态转换关系等。

核心小组后续会考虑新提案的后向兼容性影响。

ABI 稳定性

ABI 稳定性是否会影响我们使用 Swift 4.0 或 4.2 模式来维持现有源代码的兼容性?它会影响我将来改用新语言模式的能力吗?

不会。语言兼容性设置是一个纯编译时功能,用于控制源码兼容性。它不会影响到 ABI。我们不需要将 Swift 4 代码迁移到 Swift 5 模式以便使用 Swift 5 的稳定 ABI,并且如果不使用需要新运行时特性的语言功能时,也可以采用新的语言模式而不会强制要求新的系统。

ABI 稳定很美好,那么代价呢?

Good question! 我们在第一个问题里就提到过,一切都会很美好,直到下一个版本。因为 Swift runtime 现在被放到 iOS 系统里了,所以想要升级就没那么容易了。

在 ABI 稳定之前,Swift runtime 是作为开发工具的一部分,被作为库打包到 app 中的。这样一来,在开发时,我们可以随意使用新版本 Swift 的类型或特性,因为它们的版本是开发者自己决定的。不过,当 ABI 稳定后,Swift runtime 变为了用户系统的一部分,它从开发工具,变为了运行的环境,不再由我们开发者唯一决定。比如说,对应 iOS 13 的 Swift 6 的标准库中添加了某个类型 A,但是在 iOS 12.2 这个只搭载了 Swift 5 的系统中,并没有这个类型。这意味着我们需要在使用 Swift 的时候考虑设备兼容的问题:如果你需要兼容那些搭载了旧版本 Swift 的系统,那你将无法在代码里使用新版本的 Swift runtime 特性。

这和我们一直以来适配新系统的 API 时候的情况差不多,在 Swift 5 以后,我们需要等到 deploy target 升级到对应的版本,才能开始使用对应的 Swift 特性。这意味着,我们可能会需要写一些这样的兼容代码:

1
2
3
4
5
6
7
// 假如 Swift 6.0 是 iOS 13.0 的 Swift 版本
if #available(iOS 13.0, *) {
    // Swift 6.0 标准库中存在 A
    let a = A()
} else {
    // 不存在 A 时的处理
}

对于“新添加的某个类型”这种程度的兼容,我们可以用上面的方式处理。但是对于更靠近语言层面的一些东西 (比如现在已有的 Codable 这样的特性),恐怕适配起来就没有那么简单了。在未来,Deployment target 可能会和 Swift 语言版本挂钩,新的语言特性出现后,我们可能需要等待一段时间才能实际用上。而除了那些纯编译期间的内容外,任何与 Swift runtime 有关的特性,都会要遵守这个规则。

新的 Swift 功能向后是否可以部署到旧操作系统

有没有什么办法可以允许运行时支持新的 Swift 功能向后部署到旧操作系统?

可能可以使用诸如在应用程序中嵌入“填充”运行时库之类的技术来向后部署某些类型的运行时功能。但是,这并非总是可行。成功向后部署功能的能力从根本上受旧操作系统中发布的二进制组件的限制和现有错误的限制。

可以像现在一样打包新版本的 Swift runtime 到 app 里,然后指定用打包的 Swift 版本么?

不能,对于包含有 Swift runtime 的系统,如果运行的 binary 是 ABI 稳定的,那么就必须使用系统提供的 Swift。这里的主要原因是,Apple 想要保留使用 Swift 来实现系统框架的可能性:

  1. 如果允许两个 Swift runtime (系统自带,以及 app 打包的),那么这两个运行时将无法互相访问,app 也无法与系统的 Swift 框架或者第三方的 ABI 稳定的框架进行交互。
  2. 如果允许完全替换 Swift runtime,系统的 Swift 框架将执行用户提供的 Swift 标准库中的代码,这将造成重大的安全隐患。

有任何可能性让我能无视系统版本,去使用 Swift 的新特性么?

有,但是相对麻烦,很大程度上也依赖 Apple 是否愿意支持。如果你还记得 iOS 5.0 引入 ARC 时,Apple 为了让 iOS 4.3 和之前的系统也能使用 ARC 的代码,在 deployment target 选到 iOS 4.3 或之前时,会用 static link 的方式打包一个叫做 libarclite 的库,其中包含了 ARC 所需要的一些 runtime 方法。对于 ABI 稳定后的 Swift,也许可以采用类似做法,来提供兼容。

这种做法在感觉上和 Android 的 Support Library Packages 的方式类似,但是 Apple 似乎不是很倾向于提供这样的官方支持。所以之后要看有没有机会依靠社区力量来提供 Swift 的兼容支持了。

不能第一时间用上新的语言特性,必然会打击大家进行适配和使用新特性的积极性,也势必会影响到语言的发展和快速迭代,可以说这一限制是相当不利的。

所以,对于一般的 app 开发者来说,ABI 稳定其实就是一场博弈:你现在有更小的 app 尺寸,但是却被限制了无法使用最新的语言特性,除非你提升 app 的 depolyment target。

我是框架开发者,ABI 稳定后我可以用 binary 形式来发布了么?

还不能。ABI 稳定是使用 binary 发布框架的必要非充分条件。框架的 binary 在不同的 runtime 是兼容了,但是作为框架,现在是依靠一个 .swiftmodule 的二进制文件来描述 API Interface 的,这个二进制文件中包含了序列化后的 AST (更准确说,是 interface 的 SIL),以及编译这个 module 时的平台环境 (Swift 编译器版本等)。

ABI 稳定并不意味着编译工具链的稳定,对于框架来说,想要用 binary 的方式提供框架,除了 binary 本身稳定以外,还需要描述 binary 的方式 (也就是现在的 swiftmodule) 也稳定,而这正在开发中。将来,Swift 将为 module 提供文本形式的 .swiftinterface 作为框架 API 描述,然后让未来的编译器根据这个描述去“编译”出对应的 .swiftmodule 作为缓存并使用。

这一目标被称为 module stability,当达到 module stability 后,你就可以使用 binary 来发布框架了 (当然,这种 binary 框架只支持带有 ABI 稳定的 Swift runtime 的平台,也就是 iOS 12.2 及以上)。

ABI 稳定后的展望

ABI 稳定最大的受益者应该是 Apple,这让 Apple 在自己的生态系统中,特别是系统框架中,可以使用 Swift 来进行实现。在我看来,Swift ABI 稳定为 Apple 开发平台的一场革命奠定了基础。在接下来的几年里,如果你还想要关注 Apple 平台,可能下面几件事情会特别重要:

  1. Apple 什么时候发布第一个 Swift 写的系统框架
  2. Apple 什么时候开始提供第一个 Swift only 的 API
  3. Apple 什么时候开始“锁定” Objective-C 的 SDK,不再为它增加新的 API
  4. Apple 什么时候开始用 Swift 特性更新现有的 Objective-C SDK

这些事情也许会在未来几年陆续发生。面对微软从 Win32 API 向 .Net 一路迁移,到今天的 UWP (Universal Windows Platform),Google 来势汹汹的 Fuchsia 和 Dart,Swift 是 Apple 唯一能与它们抗衡的答案。相比于微软提供的泛型和并行编程模型,Google 的 Flutter 的跨平台的先天优势,Apple 平台基于 Objective-C 的 API 的易用性已然被抛开很远。虽然 Apple 在 2014 年承诺过依然维护 Objective-C,但是经过 Swift 这五年的发展,随着 Swift ABI 的稳定,什么时候如果 Objective-C 成为了继续发展的阻碍,相信 Apple 已经有足够的理由将它抛弃。

作为 Apple 平台的从业者,我们也许正处在另一个时代变革的开端。

本文由作者按照 CC BY 4.0 进行授权

Xcode 10.2

Z 字形变换