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

布景

  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 官方文档给出的解说

Swift ABI稳定性探究

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

Swift ABI稳定性探究

假如你想要了解『-ObjC』这个flag具体是做什么的,能够参阅Apple开发者文档这个链接。

但是增加这个装备参数对这台手机并不起作用。

回归到问题自身,需求先定位是存量问题还是新引进问题,庆幸的是线上无此问题,之前的测验包也没问题。那就缩小规模,是不是咱们最近的改动引起的。

2. 问题排查进程

首要缩小排查规模,猜测原因。

  1. 引进了新的runtime hook。

  2. 修正了工程的编译装备。

3. 问题排查成果

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

经过xcodebuildsettings.com/ 进行查找,成果如下

Swift ABI稳定性探究

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 上。

Swift ABI稳定性探究

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 ABI稳定性探究

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

Swift ABI稳定性探究

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 发布时,这三个组件需求各种构建,并集成到主工程中。如下图所示:

Swift ABI稳定性探究

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

Swift ABI稳定性探究

假如组件 A 的 Library Evolution 在没有启用的状况下,在组件 C 中与组件 A 相关的代码就有或许在运转时发生问题、乃至崩溃。而敞开 Library Evolution 后,就能够做到对旧版别的兼容。

咱们经过敞开封闭BUILD_LIBRARY_FOR_DISTRIBUTION装备,能够看到如下区别

如下图所示(左边为未敞开BUILD_LIBRARY_FOR_DISTRIBUTION装备,右边为敞开BUILD_LIBRARY_FOR_DISTRIBUTION装备)

Swift ABI稳定性探究

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

Swift 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