本文大纲如下:
一、背景
二、工程配置
2.1 Swift 环境配置
2.2 Swift 版本号配置
2.3 工程组件开启 Modular
2.4 工程报错解决
三、组件配置
3.1 Podspec 设置
3.2 创建桥接文件
3.3 组件编译报错解决
四、OC 和 Swift 相互调用
4.1 Swift 调用 OC
4.2 OC 调用 Swift
五、工程编译
5.1 CI/CD 平台兼容 Swift
5.2 编译耗时
5.3 本地开启二进制编译
5.4 解决编译报错
六、总结
一、背景
Swift 作为 Apple 主推的语言,其社区热度早已超过 OC,社区生态也不输于 OC,国内外的大公司,拥抱 Swift 的也越来越多。并且 Apple 官方示例均以 Swift 编写,新的技术也以 Swift 为基础,Apple 对其的推广力度更是远超 OC。相比 OC,Swift 更现代化,类型检查也更安全。虽然我们项目还是以 OC 为主,但通过混编可以使我们做新业务的时候能够使用 Swift,不再因为老的历史包袱而阻碍新技术的探索。
环境说明:下文中使用的 Xcode 版本是 14.1 (14B47b);Swift 版本是5.0。
二、工程配置
2.1 Swift 环境配置
在 Yuer 壳工程中,创建一个 Swift 文件,并同意 Xcode 自动创建桥接文件(Yuer-Bridging-Header.h),然后 Xcode 会自动帮我们完成工程的 Swift 配置:
SWIFT_VERSION = 5.0;
CLANG_ENABLE_MODULES = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Yuer/Resources/Yuer-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
下图为 Yuer 壳工程中创建的 Swift 文件:
2.2 Swift 版本号配置
在壳工程的 Build Settings 中,可以配置 Swift 的版本号: SWIFT_VERSION = 5.0 。
在2.1中创建 Swift 文件时,Xcode 会自动设置 Swift 的版本为5.0,这个版本ABI(Application Binary Interface)稳定了,我们可直接使用。
2.3 工程组件开启 Modular
从 Xcode9 开始,Swift 就支持静态库了,CocoaPods 在1.5版本发布后,也支持 Swift Static Libraries,通过配置 use_modular_headers! 可为工程组件开启 Modular:
开启后,CocoaPods 会为Pod组件生成 umbrella 、modulemap 等 support file。开启了 Modular 的组件,在 Swift 中是可以直接 import 的,不用桥接文件做中转调用,从而简化 Swift 调用 OC 的方式。
2.4 工程报错解决
报错1: This function declaration is not a prototype
解决方案:这种问题遇到了不要慌,混编过程中经常会遇到这一类问题。看上去有好多的错误,实际上可能只是一个函数声明写的不规范而已。针对该截图的错误,排查如下:
首先根据报错,定位到代码:
NSString * WebViewJavascriptBridge_js();
这行代码在非混编工程中会有一个告警,提示开发者使用函数原型,但在混编工程中就会直接报错了:this function declaration is not a prototype [-Werror,-Wstrict-prototypes],同时也会让整个 Module 编译失败,于是就产生了第二个报错:Could not build module ***。 解决的方式也很简单,按照 C 语言标准修改成函数原型即可:
NSString * WebViewJavascriptBridge_js(void);
报错2:部分组件中使用的系统库报错: **Undefine symbol: *****
从 Xcode5 开始,Xcode 自带了 AutoLinking 的能力:源码中导入系统库不用开发者手动的链接系统库,AutoLinking 会自动帮我们链接。如果项目中使用了 CocoaPods, 当导入三方 Framework 的时候,CocoaPods 也会做类似的工作,帮我们链接库中用到的系统库。但在 CocoaPods 1.5.0 支持 Swift 静态库之后,对于开启了 Modular 的混编工程,会启用严格的 header search paths 和 module map generation[注1]。如果没在 podspec 中使用 .frameworks 显式的设置组件用到的系统库,将会出现上述的链接错误。因此有两个解决方案。
解决方案1:找到所有报错的组件,并在 podspec 中显式的指定依赖的系统库。
解决方案2:在 Build Phases -> Link Binary With Libraries 中手动链接所有报错的系统库。
三、组件配置
3.1 Podspec
设置新增 Swift 源文件:
s.source_files = "Pods/Classes/**/*.{swift}"
设置 Swift 版本号:
s.swift_version = "5.0"
设置组件支持 Module:
s.pod_target_xcconfig = {'DEFINES_MODULE' => 'YES'}
3.2 创建桥接文件
在组件中,创建一个 Swift 文件,并允许 Xcode 自动创建 Bridging-Header 文件:
3.3 组件编译报错解决
报错1:三方库头文件 import 报错 '.h' file not found with** include
解决方案:这也是一类问题,使用不规范的写法导入基础组件头文件,在混编过程中就会直接报错,修改 import 写法即可解决:
#import "MJExtension.h"//或#import <MJExtension/MJExtension.h>
报错2: Cannot initialize a parameter of type '*' with an rvalue of type 'Class'**
解决方案:这种报错一般都有详细的错误信息,根据报错信息进行定位修改即可。 首先定位代码:
layoutCache[currentClass] = ivars;
然后显式的设置 currentClass 遵守 NSCopying 协议:
layoutCache[(id<NSCopying>)currentClass] = ivars;
四、OC 和 Swift 相互调用
4.1 Swift 调用 OC
在单 Target 中做 Swift 混编,可通过创建一个桥接文件 Bridging-Header.h,在桥接文件中导入 Swift 要调用的 OC 类,来实现跨语言调用。如果 Swift 文件要调用其他组件中的 OC 类,涉及到跨组件调用时,就需要两个组件都开启 Modular,开启之后就可以直接 import Module 来调用 OC。以三方库 AFNetworking 为例,开启 Modular 之后, AFNetworking 对外暴露的头文件都包含在 AFNetworking-umbrella.h 中:
同时 AFNetworking.modulemap 中也会声明 AFNetworking-umbrella.h
开启 Modular 之后,在 Swift 中调用 AFNetworking 的代码如下:
// 导入Moduleimport AFNetworking
// 调用let
manager = AFHTTPSessionManager()
let url = "**"manager.get(url, parameters: nil, headers: nil, progress: nil) { (task, data) -> Void in
// **
}
4.2 OC 调用 Swift
OC 调用 Swift,需要先 import Swift 头文件 [TargetName]-Swift.h ,这里的 TargetName 是 .podspec 中定义的组件名。[TargetName]-Swift.h 这个文件比较特殊,在工程中没对应的实体文件,是工程编译时自动生成的,所以在 import 时 Xcode 不会自动提示。 可以通过 Command + LeftClick 点击 [TargetName]-Swift.h 查看源文件:
可以看到 Swift 中的 Dog 类以及通过 @objc 修饰的属性和方法,都生成了对应的 OC 类以及 OC 属性和方法,因此 OC 才可以调用 Swift 暴露的接口。上述截图用的 OC 代码:
// 1、导入Swift头文件
#import "[TargetName]-Swift.h"
// 2、调用
- (void)invokeSwift {
Dog *dog = [Dog new];
[dog eat];
}
上述截图用的 Swift 代码:
class Dog: NSObject {
// @objc修饰的属性和方法,编译时会生成对应的OC属性和方法,以供OC调用
@objc let legNumber = 4
@objc func eat() {
print("The dog is eating")
}
}
注意:
1、Swift 中要暴露给 OC 调用的 API,需要用 @objc 修饰。如果是跨组件调用的,还需要加上 public 或 open 关键字修饰。
2、如果在 OC 中 import 桥接文件 [TargetName]-Swift.h 后 ,无法使用 Swift 暴露出来的类:
一般是因为工程还未生成 Swift 头文件([TargetName]-Swift.h),或生成的头文件中尚未包含该类,工程 Clean 一次重新编译即可。
五、工程编译
5.1 CI/CD 平台兼容 Swift
Yuer 工程本地完成适配之后,在 CI/CD 平台上进行二进制打包时出现错误:
原因是我们 CI/CD 平台输出的组件静态库都是常规的 .a 静态库,单独的.a 库 Swift 是不支持直接编译的。基础架构的同学在 Q4 对工具链做了升级,输出静态库时会自动添加一个空白的 .m 文件,并在 .podspec 中的 source_files 中追加该空白 .m。在工程使用 Cocopods 构建时,就会生成 .modulemap,从而支持 Swift 混编。目前 Yuer 工程在本地和 CI/CD 平台都支持静态库混编。
5.2 编译耗时
相比 OC 的源码编译,Swift 使用源码首次编译时,会多一个组件编译成 Module 的过程,所以整体耗时会高一些。但我们一般提审时才会源码编译,所以这个增加的耗时还可以接受。但如果是使用二进制编译,本地 Swift 全量编译耗时则不到 4min,增量编译耗时只在 1min 左右,速度非常快!
5.3 本地开启二进制编译
我们平台的工具链很早就支持了工程组件二进制编译,在 Q4 阶段工具链又兼容了 Swift,目前在本地使用工程二进制编译则非常方便。首先本地删除 Podfile.lock文件 和 Pods 文件夹,然后执行:
sh pod_install.sh --binary
Yuer 工程有200多个组件,首次全量下载二进制库耗时较长。
5.4 解决编译报错
混编过程中,经常会遇到各种报错,遇到报错不要慌,要知道即便混编工程处于正常运行的状态,Xcode也可能会显示一个错误:
但实际上,右上角的报错只是上一次编译的报错。 Yuer在混编适配过程中,遇到挺多写法不规范的古老代码,部分如下:
// 1、函数定义不规范,两个例子如下:
NSString *goldenBridge_js();
// 改成
NSString *goldenBridge_js(void);
backActionBlock:(void (^)())backActionBlock;
// 改成
backActionBlock:(void (^)(void))backActionBlock
// 2、头文件导入不规范,两个例子如下:
#import<MJExtension.h>
// 改成
#import<MJExtension/MJExtension.h>
// 使用了Ivar,但是没入头文件
// 3、重复定义,两个例子如下:
// 在不同的.h中重复定义枚举
// 在不同的.h中重复定义Block:
typedef void(^ExposureViewBlock)(UICollectionViewCell * _Nullable cell);
总的来说,遇到报错最多的就两类:
-
OC 部分组件代码不规范,导致 Swift 混编报错;
-
OC 部分组件 .h 中相互且重复引用基础组件的头文件,当在 Swift 中导入这些组件后,如果也直接使用了被导入的头文件,就可能出现重复定义的问题。
在这两类问题中,具体的报错和解决方案可能是有差异的,但排查的过程都是类似的。遇到报错后,可以参考如下的步骤进行排查:
i)、首先点开 Xcode 报错信息,查看是否有明确的报错位置,如果有则修改之
案例如下:
首先定位代码:
然后修复之:
一共有7处报错,但实际上都是因为某个被多次 import 的头文件中,使用了 MASConstraint 但是没声明导致的,加上向前声明即可。
ii)、如果没有具体的报错信息,首先要检查工程 Swift 配置是否正确,如果配置无误,大概率是缓存的问题了。
解决方式:Clean 工程 ->关闭工程->重新打开编译
六、总结
Swift 是一门非常优秀的语言,相比 OC 在安全、效率等多方面都有很大的提升。在接入的过程中虽然有许多的坑,但最终我们都逐个击破。经过一个季度的实践,我们最终在 Yuer 项目中实现了用 Swift 开发业务。未来,我们将在 Swift 上继续加大投入,全面拥抱 Swift。
说明:
[注1] :blog.cocoapods.org/CocoaPods-1…
Modules 官方文档: clang.llvm.org/docs/Module…