背景
自 Swift 诞生以来,逐渐见证其从饱受诟病到日渐欠缺。在苹果的全力推动下,耳濡目染地把开发反对核心从 Objective-C 转向 Swift,在业界的呼声也越演越烈。当咱们相继迎来 ABI 稳固、Module stability、Library evolution 等性能后,咱们期盼已久的 Swift 未然到来,决然启动了京东 App 的混编之旅。咱们仍然保持操之过急,后期对 Swift 技术做了诸多调研工作,具体可见《Swift 环境及编译优化调研》。2020 年 7 月京东 App 的首个混编版本上线苹果商店,实现了组件内和主工程的混编工作;近期,咱们实现了对京东组件化管理工具(iBiuTool)的革新,混编组件化性能正式落地,这也标记着京东 Swift 混编根底反对建设结束。然而,Just the beginning…
期待的 Swift 曾经到来
2.1 ABI 稳固
Swift 5.0,提供 ABI 稳固,解决了 Swift runtime 的版本兼容问题。这意味着通过 Swift 5.0 及以上的编译器编译进去的二进制,就能够运行在任意 Swift 5.0 及以上的 Swift runtime 上。ABI 稳固后,Swift runtime 和规范库曾经植入 macOS 10.14.4、iOS 12.2、watchOS 5.2 及以上零碎中。依据苹果官网数据,截止到 2020 年 12 月 15 日,四年内公布的 iPhone 设施中 iOS 13 及以上占比已达 98%。
另外,ABI 稳固还带来了性能上的晋升。因为 Swift runtime 曾经被深刻的集成在了设施的操作系统中,并且联合零碎层做了许多优化,这就使得 Swift 程序具备更快的启动速度、更好的运行性能,以及更少的内存占用量。
2.2 Module Stability
Swift 5.1,反对 Module Stability,解决模块间编译器版本兼容的问题。这意味着应用不同版本编译器构建的 Swift 模块能够在同一个应用程序中一起应用。即便某些三方库的 Swift 编译器版本与你所应用的不同,也不会存在编译问题。官网文档中举了一个非常失当的例子,应用 Swift 6 构建的 framwork,能够被 Swift 6 和将来的 Swift 7 编译器失常应用。所以这个进化对于开发者来说,相对是一件十分美妙事件。
在 Swift 中有一个 .swiftmodule 文件,它是一种二进制文件,次要蕴含模块中的数据信息和外部编译器的数据结构。因为外部编译器的数据结构的存在,同一个模块编译的 swiftmodule 文件在不同版本的编译器中都是不一样的。这也就是为什么在某个版本编译器中编译的二进制文件,在另一个版本编译器中无奈被导入应用的起因。
Module Stability 解决了这个问题,在模块稳固后,存储模块信息的文件曾经代替为 swiftinterface 格局了。它是一个文本格式的文件,它蕴含所有 public 或者 open 的 API 以及一些隐式的代码或者 API,还包含 swiftinterface 的版本、生成此 swiftinterface 的编译器版本,以及 Swift 编译器将其作为模块导入时所需的命令行标记的子集。而且这些 API 与源代码很相似,通过源码稳固实现了模块稳固。
2.3 Library Evolution
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 后,就可能做到对旧版本的兼容。
混编的形式
京东 App 基本上是一个基于 Cocoapods 实现的组件化工程,总的来看须要划分为两个场景:一、主工程的混编;二、各组件内的混编。在这两个场景中,对 Swift 引入 ObjC 和 ObjC 引入 Swift 又做了不同的解决。
3.1 工程中 – Swift 调用 ObjC
在主工程的 Target 下,须要通过桥接头文件的形式,将 ObjC 的头文件裸露给 Swift 进行应用。
- 创立桥接文件(-Bridging-Header.h)
- 确保 Build Setting 中 SWIFT\_OBJC\_BRIDGING_HEADER 为该桥接文件的门路
- 将须要引入到 Swift 的 ObjC 的头文件增加进去
3.2 工程中 – ObjC 调用 Swift
在主工程的 Target 下,能够通过引入 Swift Module 的 ObjC Interface Header 的形式,在 ObjC 中应用 Swift。因为 Swift Module 的缘故,所以引入一个文件,便能够应用该模块下的所有 Swift 文件。须要留神的是,这个头文件的命名默认是 ”ProjectName-Swift.h”,如果工程名中有一些 nonalphanumeric 字符,则会被替换为下划线。
- 确保 Build Setting 中 SWIFT\_OBJC\_INTERFACE\_HEADER\_NAME 的配置正确
- 在 ObjC 中引入该模块的 Swift 头文件,#import “XXX-Swift.h”
- 若在 ObjC 的 .h 中引入,则能够通过向前申明的形式,@class XXX
3.3 组件内 – Swift 调用 ObjC
在同一个 .framework 或者 .a 中实现 Swift 调用 ObjC,通过 Bridging-Header 的形式是无奈解决的。如果你尝试应用 Bridging-Header 的形式,并且通过 .podspec 对 Bridging-Header 进行配置写入。只会有短暂性的编译胜利,最终将会报错:
通过官网文档中的近一步查证,发现在同一个 framework 中的 Swift 想要引入 ObjC,须要将该 ObjC 文件导入到其 umbrella-header 文件中。这样 Swift 模块就能够对 umbrella-header 中向外裸露的类进行调用了。另外,官网文档中还提到 DEFINES_MODULE 要配置为 YES,这样整个组件就能够作为一个模块被内部导入应用了。
3.4 组件内 – ObjC 调用 Swift
在同一个 .framework 或者 .a 中实现 ObjC 调用 Swift,仍然须要通过引入 Swift Module 的 ObjC Interface Header。
- 确保 Build Setting 中 SWIFT\_OBJC\_INTERFACE\_HEADER\_NAME 的配置正确
- 在 ObjC 中引入该模块的 Swift 头文件,.framework 中为 #import <XXX/XXX-Swift.h>,.a 中为 #import “XXX-Swift.h”。
- 若在 ObjC 的 .h 中引入,则能够通过向前申明的形式,@class XXX
组件内混编
4.1 组件内混编实施方案
参照上文中梳理的大体计划,便能够对京东 App 组件进行混编施行。京东组件通过本人的工具进行组件治理的,因为历史起因,某些方面的性能还不能齐全反对,比方 modulemap、module stability、new build setting。所以这一些问题须要绕过,这些问题文章前面会针对阐明。具体的施行步骤如下:
- 组件内增加 Swift 文件,且不须要创立桥接文件
- podspec 中 source_files 配置中增加 swift 项
- ObjC 调用 Swift
- 在苹果官网文档中,举荐配置 DEFINES_MODULE = YES,并通过 #import <XXX/XXX-Swift.h> 的形式导入 Swift Module。但在京东组件中动静库是以 .framework 模式存在的,须要以 #import <XXX/XXX-Swift.h> 的形式导入 Swift Module;而动态库是以 .a 模式存在的,须要以 #import “XXX-Swift.h” 的形式导入
- 值得注意的是,要确保你的 Swift 类为 Public 或 Open 的拜访权限,否则在 Swift Module 之外的 ObjC 文件中是无论如何都不能调用的。对于 ObjC 中想要应用属性和函数,须要标记 @objc,它会通知编译器该属性或者函数可能利用于 Objective-C 代码中。而且标有 @objc 个性的类必须继承自 ObjC 的类。
- Swift 调用 ObjC
- Swift 模块想要调用 ObjC 就须要将 ObjC 的头文件裸露在 umbrella-header 中。这样就须要在 podspec 中将 public\_header\_files 配置中增加要裸露的 ObjC 头文件后,供 Swift 进行调用。
4.2 组件内混编通信计划
依照上述计划施行后,组件内的通信归为 ObjC 调用 Swift 和 Swift 调用 ObjC 两个方面。具体通信形式如下图所示:
组件间混编
5.1 Swift 调用 ObjC
Swift 调用 ObjC API 前,首先须要通过 import module 语法找到对应的模。Module 机制是在 2013 年退出了 Xcode 中,目标是为了晋升编译速度,解决 C、C++ 中 #include 机制的一些遗留问题。具体是如何解决的,能够看下这两篇文章:对于 objective-cmodules 和 autolinking(1)、Clang 官网文档(2)。
咱们须要解决的问题是如何让编译器找到 Module,通过查看 Clang 官网的文档,咱们发现:
- 如果要反对 Module,必须提供一个 module.modulemap 文件,用来申明模块与头文件之间的映射关系
- 针对 framework,Clang 会通过指定门路查找命名为 module.modulemap 的文件:.framework/Modules/module.modulemap
module.modulemap 文件中的内容大抵如下,次要是用来申明模块与头文件之间的映射关系,反对 import module 形式调用。
framework module STStaticBasicStableModule {
umbrella header "STStaticBasicStableModule-umbrella.h"
export *
module * {export *}
}
module STStaticBasicStableModule.Swift {
header "STStaticBasicStableModule-Swift.h"
requires objc
}
找到模块,并且晓得模块有哪些头文件后,就能够拜访组件提供的类、办法了。
5.2 Swift 调用 Swift
咱们晓得 ObjC 代码之间调用 API 是通过头文件的模式,但 Swift 是没有头文件的,它应用一个二进制格局的文件(.swiftmodule)来代替头文件,这个文件中蕴含了 Swift 模块的所有 API、inlinable function bodies。
编译器会去哪找 swiftmodule 文件呢?咱们在 swift 源码中找到了一些蛛丝马迹:
// SerializedModuleLoader.cpp
void SerializedModuleLoaderBase::collectVisibleTopLevelModuleNamesImpl(SmallVec torImpl<Identifier> &names, StringRef extension) const {
// ...
forEachModuleSearchPath(Ctx, [&](StringRef searchPath, SearchPathKind Kind,
bool isSystem) {switch (Kind) {
// ...
case SearchPathKind::Framework: {
// 源码中的正文及相干代码阐明了 swiftmodule 在 framework 中的查找机制
// Look for:
// $PATH/{name}.framework/Modules/{name}.swiftmodule/{arch}.{extension}
forEachDirectoryEntryPath(searchPath, [&](StringRef path) {// ...});
return None;
}
}
llvm_unreachable("covered switch");
});
}
.swiftmodule 是一个序列化后的二进制文件,从文件名 SerializedModuleLoader.cpp 能够猜想这个是负责加载.swiftmodule 文件的。另外上述代码中也阐明了针对 framework,Swift 编译器如何查找.swiftmodule。
同时为了保障组件反对 x86、arm 架构下编译,咱们还须要将不同架构编译生成的 swiftmodule 文件手动合并到最终的 framework 中。
5.3 ObjC 调用 Swift
编译器会通过咱们编写的 Swift 代码生成 xxx-Swift.h,这样 ObjC 就能够通过这个头文件拜访 Swift 的 API 了。咱们能够通过两种 import 形式调用 Swift API:
- @import STStaticBasicStableModule
-
import “STStaticBasicStableModule-Swift.h”
还记得下面 modulemap 中的 STStaticBasicStableModule.Swift 吧,@import STStaticBasicStableModule 在找到模块后,会通过 modulemap 文件中申明找到 STStaticBasicStableModule-Swift.h:
module STStaticBasicStableModule.Swift {
header "STStaticBasicStableModule-Swift.h"
requires objc
}
xxx-Swift.h 也须要反对多架构,咱们须要把不同架构下生成的 xxx-Swift.h 内容合并到一个文件中,最终合并的 xxx-Swift.h,去掉局部代码,大抵构造是这个样子:
#ifndef TARGET_OS_SIMULATOR
#include <TargetConditionals.h>
#endif
#if TARGET_OS_SIMULATOR
// Release-iphonesimulator/Swift Compatibility Header/XXX-Swift.h
#if 0
#elif defined(__x86_64__) && __x86_64__
// __x86_64__
#elif defined(__i386__) && __i386__
// __i386__
#endif
#else
// Release-iphoneos/Swift Compatibility Header/XXX-Swift.h
#if 0
#elif defined(__arm64__) && __arm64__
// __arm64__
#elif defined(__ARM_ARCH_7A__) && __ARM_ARCH_7A__
// __ARM_ARCH_7A__
#endif
#endif // TARGET_OS_SIMULATOR
5.4 Module stability & Library evolution
文章开篇说过,它们是 Swift 5.1 新增的 2 个对于二进制稳固的个性,能够反对公布和共享 framework。只有当你的库要独立于客户端进行构建的状况下,才须要开启 Build Libraries for Distribution 选项,而且 Module Stability 和 Library Evolution 就会同时失效。通常咱们在开发二进制库时,最好尽早关上此开关,以提供任何二进制兼容性的保障。
如果不反对这 2 个个性,可能会呈现的问题:
User1 应用 Xcode 11.2 公布了根底组件,User2 依赖了这个组件,并应用 Xcode 11.7 编译,后果:编译报错 Module compiled with Swift 5.1.2 cannot be imported by the Swift 5.2.4compiler
User1 给构造体中新增了一个属性,而后公布了新版本组件,依赖该组件的上游较多,User1 须要周知所有依赖方依赖最新版本从新编译,否则可能会引发运行时解体
- Xcode 中设置 build setting 中的 BUILD\_LIBRARY\_FOR_DISTRIBUTION 为 YES
- 须要反对 New Build System。
5.5 组件间混编通信计划
依照上述计划施行后,组件间的通信归为 ObjC 调用 Swift 和 Swift 调用 ObjC,以及 Swift 调用 Swift 三个方面。具体通信形式如下图所示:
京东主工程混编反对计划
6.1 动态编译的问题
因为咱们之前是纯 ObjC 的开发环境,所以即便实现了组件内混编,京东组件化的主工程(或者组件的 Example 工程)也并不能胜利编译。起因在于它们还不反对 Swift 混编环境,在编译时可能会报相似谬误:
或者
上述的错误信息阐明,编译器不能主动链接到 Swift 相干的一些动态库和动静库。而这些资源是存在于 Xcode Toolchains 下的,在本地的门路为:
- /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos
- /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/iphoneos
接下来将这些资源门路配置在工程中,个别地须要在 Build Settings -> Library Search Paths 中增加:
- “$(TOOLCHAIN\_DIR)/usr/lib/swift/$(PLATFORM\_NAME)”
- “$(TOOLCHAIN\_DIR)/usr/lib/swift-5.0/$(PLATFORM\_NAME)”
6.2 动静库加载的问题
当配置好可链接资源的门路之后,就能够胜利编译了。但在启动时,动静库加载的问题将会引起程序的解体。诸如以下谬误:
如果你的设施是 iOS 12.2 及以上,可能报错如下:
在 Build Settings -> Runpath Search Paths 中首行增加配置 /usr/lib/swift 配置,特地要留神的是只能在首行配置能力解决问题。
如果你的设施是 iOS 12.2 以下,可能报错如下:
iOS 12.2 以下,将 Build Settings -> Always Embed Swift Standard Libraries 设置为 YES。
这两个问题都是在利用启动后,动静库加载时,产生的解体。那为什么要以 iOS 12.2 为分水岭呢?这就是从 iOS 12.2 Swift 曾经实现 ABI 稳固了。所以上述两处的解决方案缺一不可,因为它们别离针对 ABI 稳固前后的零碎版本。这些配置实现后,京东组件的主工程(或者组件的 Example 工程)就曾经齐全反对 Swift 混编环境了。另外,还有一种更加便捷计划也能够达到同样的成果。
6.3 一键配置混编环境
除了 Xcode Build Settings 配置的形式,还能够通过在工程中新建一个 Swift 文件(文件中无需增加任何代码),通过这种形式 Xcode 会主动实现局部环境配置,可能解决动态编译问题和局部设施的动静库加载问题。另外,还须要解决 iOS 12.2 以下 Swift 动静库加载的问题。将 Always Embed Swift Standard Libraries 设置为 YES。
如果通过 Xcode Build Settings 配置的形式,从 Xcode 11 beta 4 开始,就须要在工程配置中增加 swift-5.0 的新配置项了。但如果通过新建 Swift 文件的形式,就不用更新配置,它的劣势在于机动性好。
京东是一个规范的组件化利用,主工程中无代码实现,所以不须要实现混编代码,仅须要使其反对 Swift 开发环境即可。因而 Xcode Build Settings 配置的形式可能满足需要,无需多余文件,在工程的简洁性上更好。
通信计划总结
最初,对京东 App 中涵盖的混编通信形式做个汇总。以组件内、组件间,以及主工程的混编模式为根底,将整体的混编通信形式汇总如下:
作者: 王彦昌、姚琦、林晓峰
参考文献
*(1)https://onevcat.com/2013/06/new-in-xcode5-and-objc/# 对于 objective-cmodules 和 autolinking
*(2)https://clang.llvm.org/docs/Modules.html
*https://developer.apple.com/documentation/swift/imported\_c\_and\_objective-c\_apis/importing\_objective-c\_into_swift
*https://developer.apple.com/documentation/swift/imported\_c\_and\_objective-c\_apis/importing\_swift\_into_objective-c
* http://clang.llvm.org/docs/Modules.html
* https://swift.org/blog/library-evolution/
* https://swift.org/blog/abi-stability-and-more/
*https://forums.swift.org/t/plan-for-module-stability/14551
*https://forums.swift.org/t/learning-about-swifts-lack-of-header-files/13215/5
* https://github.com/apple/swift
*https://github.com/apple/swift-evolution/blob/master/proposals/0260-library-evolution.md
欢送点击【 京东科技 】,理解开发者社区
更多精彩技术实际与独家干货解析
欢送关注【京东科技开发者】公众号