Swift ABI稳定性探究

9,700 阅读8分钟

作者介绍:姚亚杰,来自出行研发部-架构组,专注于移动端业务架构方向。

背景

  1. 故事还要从一个线下bug说起,起源是测试反馈App点击设置无法进行路由跳转,型号: iPhone 8plus, OS版本: 11.3.1 。
1. 问题定位

经过定位是路由SDK如下代码引起异常导致的。

public func la_matchClass() -> AnyClass? {

    if let cls: AnyClass  = NSClassFromString(self) {
        return cls
    }
    return nil
}

身边其他版本机型都是正常返回,此台机器返回了nil 。

我们先来看看 Apple 官方文档给出的解释

NSClassFromString只能返回当前已经加载过的Class。所以在某些类并没有被使用过或加载过时,就会得到nil的结果。比如想要用NSClassFromString获得一个静态库里的类,通常就会遇到这种情况。一般的解法就是加上 Other Linker Flags,双击添加一个『-ObjC』。

如果你想要了解『-ObjC』这个flag具体是做什么的,可以参考Apple开发者文档这个链接

但是添加这个配置参数对这台手机并不起作用。

回归到问题本身,需要先定位是存量问题还是新引入问题,庆幸的是线上无此问题,之前的测试包也没问题。那就缩小范围,是不是大家最近的改动引起的。

2. 问题排查过程

首先缩小排查范围,猜测原因。

  1. 引入了新的runtime hook。

  2. 修改了工程的编译配置。

3. 问题排查结果

最后经过排查,发现问题是修改了主工程的一个Build Setting配置,BUILD_LIBRARY_FOR_DISTRIBUTION 由NO修改为了YES导致的。 我们回撤之后,发现该机型正常进行跳转了。那么这 BUILD_LIBRARY_FOR_DISTRIBUTION 为什么会有如此影响呢?

通过xcodebuildsettings.com/ 进行搜索,结果如下

BUILD_LIBRARY_FOR_DISTRIBUTION = YES 配置之后,在较旧的平台上,某些功能(例如构建在 NSClassFromString() 之上的功能)将无法像预期的那样与需要运行时初始化的类一起工作。

BUILD_LIBRARY_FOR_DISTRIBUTION 是为了支持模块接口文件生成与模块演变的。下边我们详细的介绍下Swift库演进相关概念。

ABI稳定性

Swift 5.0,提供 ABI 稳定,解决了 Swift runtime 的版本兼容问题。这意味着通过 Swift 5.0 及以上的编译器编译出来的二进制,就可以运行在任意 Swift 5.0 及以上的 Swift runtime 上。

ABI稳定性优势

  1. Apple OS的ABI稳定性意味着部署到这些操作系统即将发布的应用程序将不再需要在应用程序包中嵌入Swift标准库和“叠加”库,从而缩小其下载大小;
  2. Swift运行时和标准库将随操作系统一起提供,就像Objective-C运行时一样。

ABI稳定性弊端

  1. 因为 Swift runtime 现在被放到 iOS 系统里了,所以想要升级就没那么容易了。集成到OS的Swift runtime只能伴随iOS系统更新才会更新,不像稳定之前我们自己打包的会随着App更新而更新。
  2. 对于“新添加的某个类型”这种程度的兼容,在未来,Deployment target 可能会和 Swift 语言版本挂钩,新的语言特性出现后,我们可能需要等待一段时间才能实际用上。而除了那些纯编译期间的内容外,任何与 Swift runtime 有关的特性,都会要遵守这个规则。

模块稳定性

Swift 5.1,支持 Module Stability,解决模块间编译器版本兼容的问题。这意味着使用不同版本编译器构建的 Swift 模块可以在同一个应用程序中一起使用。即使某些三方库的 Swift 编译器版本与你所使用的不同,也不会存在编译问题。官方文档中举了一个十分恰当的例子,使用 Swift 6 构建的 framework,可以被 Swift 6 和未来的 Swift 7 编译器正常使用。所以这个进化对于开发者来说,绝对是一件非常美好事情。

在 Swift 中有一个 .swiftmodule 文件,它是一种二进制文件,主要包含模块中的数据信息和内部编译器的数据结构。由于内部编译器的数据结构的存在,同一个模块编译的 swiftmodule 文件在不同版本的编译器中都是不一样的。这也就是为什么在某个版本编译器中编译的二进制文件,在另一个版本编译器中无法被导入使用的原因。

Module Stability 解决了这个问题,在模块稳定后,存储模块信息的文件已经替代为 swiftinterface 格式了。它是一个文本格式的文件,它包含所有 public 或者 open 的 API 以及一些隐式的代码或者 API,还包括 swiftinterface 的版本、生成此 swiftinterface 的编译器版本,以及 Swift 编译器将其作为模块导入时所需的命令行标志的子集。而且这些 API 与源代码很类似,通过源码稳定实现了模块稳定。

.swiftinterfaceModule stability模块的稳定性,是swift5.1推出解决模块之间编译器版本兼容问题。这就意味着不同版本编译器构建的swift模块可以在同一个应用程序中一起使用。

实际上.swiftinterface.swiftmodule是差不多的,.swiftinterface多了一个解决兼容性的东西。 编译速度上.swiftinterface会更慢一些;在编译期间没有模块兼容性问题的时候,优先用.swiftmodule

// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.7.2 effective-4.1.50 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
// swift-module-flags: -target x86_64-apple-ios9.3-simulator -enable-objc-interop -enable-library-evolution -swift-version 4 -enforce-exclusivity=checked -Onone -module-name LABTest_Example
// swift-module-flags-ignorable: -enable-bare-slash-regex
import Swift
import UIKit
import _Concurrency
import _StringProcessing

库进化

Swift 5.1, 支持 Library Evolution,解决了二进制库向下兼容的问题。在 Library Evolution 特性开启的状态下,二进制库某些场景下的 API 更新后,就会自动实现对旧版本库的兼容。Library Evolution 可以在不破坏二进制兼容性的情况下对库进行某些修改。

举例来具体说明一下这个问题。组件 B 和组件 C 都依赖了组件 A,他们的组件版本都是 v1.0。主工程的 v1.0 发布时,这三个组件需要各种构建,并集成到主工程中。如下图所示:

当主工程 v2.0 发布时,组件 A 对组件 B 在 v1.0 版本所使用的 API 进行了一些 resilient 的修改,但这些修改并没有影响到组件 C。所以,组件 B 在构建二进制库时,就需要更新依赖的组件 A 到 v2.0 版本。而组件 C 没有功能修改,则不需要更新依赖和发布新版本。然后,他们都集成到 v2.0 版本的主工程中。

如果组件 A 的 Library Evolution 在没有启用的情况下,在组件 C 中与组件 A 相关的代码就有可能在运行时产生问题、甚至崩溃。而开启 Library Evolution 后,就能够做到对旧版本的兼容。

我们通过开启关闭BUILD_LIBRARY_FOR_DISTRIBUTION配置,可以看到如下区别

如下图所示(左边为未开启BUILD_LIBRARY_FOR_DISTRIBUTION配置,右边为开启BUILD_LIBRARY_FOR_DISTRIBUTION配置)

找到abi差异文件打开之后进行比对

{
        "kind": "TypeDecl",
        "name": "XLBaseViewController",
        "printedName": "XLBaseViewController",
        "children": [
          {
            "kind": "Function",
            "name": "viewDidLoad",
            "printedName": "viewDidLoad()",
            "children": [
              {
                "kind": "TypeNominal",
                "name": "Void",
                "printedName": "()"
              }
            ],
            "declKind": "Func",
            "usr": "c:@M@LABTest@objc(cs)XLBaseViewController(im)viewDidLoad",
            "mangledName": "$s7LABTest20XLBaseViewControllerC11viewDidLoadyyF",
            "moduleName": "LABTest",
            "overriding": true,
            "isOpen": true,
            "objc_name": "viewDidLoad",
            "declAttributes": [
              "Dynamic",
              "ObjC",
              "Custom",
              "Override",
              "AccessControl"
            ],
            "funcSelfKind": "NonMutating"
          },

通过Demo工程差异比对,开启之后主要是增加了swiftInterface文件,依靠.swiftinterface的文本文件来描述API,让未来的编译器根据这个描述去“编译”出对应的.swiftmodule作为缓存并使用。另外就是abi.json文件。可以看到abi.json中结构化的声明了类及其暴露的方法等。为什么调用 NSClassFromString 失败,还要看下边的解释,即与Objective-C互操作上来讲。

与Objective-C互操作

开启 Library Evolution 之后,在与Objective-C互操作性上 需要注意:

如果您的框架定义了一个open类,则客户端代码中的子类定义必须执行运行时初始化,以应对基类中的弹性变化,例如添加新的存储属性或插入超类。此初始化由Swift运行时在幕后处理。

然而,如果一个类需要运行时初始化,那么在较新的平台版本上运行时,它只能对Objective-C运行时可见。这样做的实际结果是,在较旧的平台上,某些功能(例如构建在 NSClassFromString() 之上的功能)将无法像预期的那样与需要运行时初始化的类一起工作。此外,需要运行时初始化的类不会出现在由 Swift 编译器生成的 Objective-C 生成的标头中,除非部署目标设置为足够新的平台版本。详细信息请查看 www.swift.org/blog/librar…

结论

如果低版本开启了 BUILD_LIBRARY_FOR_DISTRIBUTION = YES会有Runtime方面的影响,为了以后二进制的演进,就需要修改技术方案或者提高最低iOS版本限制了。

在之前的版本中实现逻辑是

 class func addRouter(_ patternString: String, classString: String) {
      let clz: AnyClass? = classString.trimmingCharacters(in: CharacterSet.whitespaces).la_matchClass()
       if let routerable = clz as? LARouterable.Type {
          self.addRouter(patternString.trimmingCharacters(in: CharacterSet.whitespaces), handle: routerable.registerAction)
       } else {
          assert(clz as? LARouterable.Type != nil, "register router error, please implementation the LARouterable Protocol")
       }
  }

这里主要是从通过协议找到类,再根据类找到实现的 registerAction方法获取实例方法,我们可以通过外部传入registerAction的方式即可解决这其中的无法找到遵循协议类与registerAction获取实例的相关逻辑。

因为所有的路由组件,不管是Objective-C还是Swift,核心的实现逻辑都是 NSClassFromString ,所以我们暂时回退了该选项。待用户OS版本13.0 的比例上升到95%的比例之后,统一升级最低版本限制,开启该选项。

参考

www.swift.org/blog/librar…

www.swift.org/blog/abi-st…

forums.swift.org/c/evolution…

zhuanlan.zhihu.com/p/349967113