应用 Swift 静态库的各种坑

avatar
@雪球财经

图片

蛋卷基金是雪球旗下的一个独立基金销售平台。 蛋卷基金(以下简称蛋卷)算较早使用Swift的项目,从立项的Swift 2.0版本到目前的4.0.3,一直在等待静态打包这个feature。终于在Xcode9升级后,Swift支持了的静态打包。

我们知道,动态库是为了实现代码共享、资源共用。但在iOS中,苹果是不推荐使用动态库,这会影响应用的启动时间(注1)。而且动态库数量过多,还可能在 iOS 9 的设备上造成 dyld 的 crash (注2)。

由于蛋卷是采用CocoaPods和Carthage混合管理依赖包的方式,改造需要对CocoaPods和Carthage管理的依赖库分别进行。在我们着手之前,已经有其他公司通过手动改造脚本化,使用Xcodeproj来进行静态库打包。而我们刚好等到了CocoaPods1.5的发布(支持Swift Static Libraries):)。

| CocoaPods

在升级CocoaPods最新版后,修改Podfile:

图片

主要是去掉use_frameworks! ,改为use_modular_headers!,经pod install,Xcode一片冒红,果然还是有坑啊!

错误如下:

图片

几经折腾,发现问题在于:

  • bridging-header import的lib header和lib Module有循环引用;

解决方式很简单,在bridging-header中去掉各个依赖包的引用就可以。不过要搞明白原因就费老劲了。

首先,CocoaPods1.5新增的属性use_modular_headers!,是将所有的pods转为 Modular。Modular是可以直接在Swift中 import 的,不需要再经过 bridging-header 的桥接。故此有重复引用的问题。

对于Modular Headers的问题,CocoaPods Blog (注3) 有提到。早期,为了让大部分libraries能够打包成Pods,CocoaPods利用Header Search Paths,使任何通过un-namespaced引入的头文件能成功编译。当 pod install 的时候,pod 的路径都会设置在Header Search Paths而不是User Header Search Paths,这样就算用#import<>引用也不会报错。

其次,之前Podfile中使用的关键字use_frameworks! 是将lib打包成umbrella framework。现在改为static library,需要手动设置 DEFINES_MODULE 为true开启Modular或者通过use_modular_headers!   ,而这会导致一下pod的header查找出错。为此,CocoaPods 1.5取消 Modular 的强制转换。

这需要解释一下 framework 的概念。

iOS中所谓静态库是包含了static library和static framework两种,动态库也有dynamic library和dynamic framework之分。Library 和 Framework 可以说是一个包含关系,Framework 其实是一个特殊形式Bundle,不仅包含 share library 还有 xib、images、property list等。

上述的 framework 是属于standard framework,umbrella framework就是CocoaPods使用的,理解为包含framework的framework。具体参考这两篇文章 Dynamic Library Programming Topics (注4) 和 Umbrella Header在framework中的应用 (注5)。

说完framework,再解释一下umbrella header与Modular headers,它们都是llvm Module系统中的概念。关于llvm Module 系统,Modules for Swift 这篇文章 (注6) 提到:

图片

同时Swift Module System算是 llvm Module 系统的定制版。

去掉bridging-header引用后,还有一个体力活需要修改。由于Swift是带有隐藏命名空间的,通过bridging-header引用的lib包是在工程命名空间下,改为 Modular 后,会触发相应的方法丢失的错误,需要在文件头部手动import相关library。

另外一个小问题就是,当你的Swift project中恰好有OC的代码(不是依赖库),而如果该代码中引入了一个Modular library的话。那么,恭喜你,这也会触发 modular header not define 的 Error。因为OC代码要通过bridging-header才能被Swift调用,当OC代码又引用了Modular library时,会导致 modular 引用冲突,编译失败。

解决方式是对应的library改modular_headers => false,  最合适的方式还是改为Swift 文件。

至此,CocoaPods的依赖包静态化就差不多了。

| Carthage

Carthage官方目前还没有支持Swift静态库,不过官方提供一篇python脚本化的静态库打包方式。利用libtool工具创建swift静态库,基本没有太大问题,过程就不赘述了。需要注意的是生成的static libraries不再需要embed到Framework中,添加到 Link Binary with Libraries 就Ok啦!

| 效果

目前项目内的OC和Swift的依赖全部打包成静态库,OC部分已经在5.3上线,Swift库的静态包还没合进主分支。

  • 全部lib改为静态打包, 在iPhone 6 Plus真机测试,premain 消耗从 5900+ms 降低到 1300+ms;

  • 通过itunesconnect统计,使用OC静态库的下载包从 35.2M 减少到 30M,安装包从 57.9M 减少到 46.9M;

| 参考

注1: Optimizing App Startup Time 

developer.apple.com/videos/play…

注2: Crash 

github.com/artsy/eigen…

注3: CocoaPods Blog 

blog.cocoapods.org/CocoaPods-1…

注4: Dynamic Library Programming Topics 

developer.apple.com/library/arc…

注5: Umbrella Header在framework中的应用 blog.startry.com/2015/08/25/…

注6: Modules for swift 

andelf.github.io/blog/2014/0…

| 还有一件事

雪球的工程师团队在招聘,Java 工程师,运维开发工程师,测试开发工程师,算法工程师,有意的同学可以查看原文看看具体的职位和要求,就等你了。