Swift Static Libraries迁移实践

8,631 阅读8分钟

背景介绍:

二维火云收银iOS客户端使用了Objective-C和Swift混编,在Xcode9(2017年9月发布)之前苹果不支持使用Swift Static Libraries。 同时,我们使用了CocoaPods进行项目管理,对于Swift+CocoaPods的项目直到2018年4月发布的Cocoapods1.5.0才官宣支持把Swift Pods构建成Static Libraries。所以在CocoaPods1.5.0之前我们一直使用的是Dynamic frameworks。

动态库与静态库的区别、优劣不在本文讨论范围之内,相关的文章网上可以搜到很多。本文主要记录了我们为什么要转向转向Swift Static Libraries ,以及迁移过程遇到的一些问题和思考。

Dynamic frameworks的困境

  • 很多第三方库(如WechatSDK)是以静态库的方式提供给开发者,这就导致我们没有办法直接接入。在执行pod install 的时候会产生has transitive dependencies that include static binariesd的error。所以一直以来我们都是苦逼的作一层包装,把第三方库做成私有的Dynamic framework然后接入到工程中(如果谁有更好的处理方法,希望得到指导)。然而,就是这个二次包装的过程我们也踩了很多坑,其中印象深刻的是Archive的时候遇到了bitcode问题。调试运行正常的代码在最终打包预备发布的时候遇到下面错误:
bitcode bundle could not be generated because xxx was built without full bitcode.
All object files and libraries for bitcode must be generated from Xcode Archive or 
Install build for architecture armv7 

记得当时翻了一下午Google,最终在这里找到了答案,是对BITCODE_GENERATION_MODE没有正确设置,在这里再次感谢一下文章作者。

  • 公司其他业务线大多使用的是Static Libraries,不管是出于代码复用或者产品需求,想要使用他们的SDK也会遇到第一个问题。要求他们同时提供和维护动态库版本也不太现实,这就产生了我们与其他业务线在技术栈上的一个差异。

迁移过程

阶段一. 春天来了

当看到CocoaPods官宣“CocoaPods 1.5.0 — Swift Static Libraries”时,我们欣喜的以为迁移Static libraries的时机终于到了。
官宣官宣:

With CocoaPods 1.5.0, developers are no longer restricted into specifying use_frameworks! in their Podfile in order to install pods that use Swift. Interop with Objective-C should just work. However, if your Swift pod depends on an Objective-C, pod you will need to enable "modular headers" (see below) for that Objective-C pod.

简单来说就是:
从CocoaPods 1.5.0起,开发者不再限定于在他们的Podfile中使用use_frameworks!来安装使用Swift的Pods。 与Objective-C的互操作应该像之前一样能够正常工作。 但是,如果你的Swift pod依赖于某个Objective-C的 pod,你需要为该Objective-C pod启用“modular headers”。

感觉我们的春天终于来了,于是趁着项目缓冲的功夫,召集大家搞起!搞起!

迁移步骤基本参照了CocoaPods的官方指导,具体有以下几点:

  1. 升级Cocoapods到最新版本,我们实际开始迁移工作时,CocoaPods最新版本是1.5.3
  2. 修改Gemfile中对Cocoapods的版本约束,删除原有的Gemfile.lock
  3. 修改Podfile, 去掉use_frameworks!(一直以来的噩梦!), 改用添加use_modular_headers!,这将开启严格的header search path。官方原文是:

When CocoaPods first came out many years ago, it focused on enabling as many existing libraries as possible to be packaged as pods. That meant making a few tradeoffs, and one of those has to do with the way CocoaPods sets up header search paths. CocoaPods allowed any pod to import any other pod with un-namespaced quote imports.
For example, pod B could have code that had a #import "A.h" statement, and CocoaPods will create build settings that will allow such an import to succeed. Imports such as these, however, will not work if you try to add module maps to these pods. We tried to automatically generate module maps for static libraries many years ago, and it broke some pods, so we had to revert. In this release, you will be able to opt into stricter header search paths (and module map generation for Objective-C pods).

翻译过来是:

当CocoaPods多年前首次问世时,它专注于使尽可能多的现有库被打包为pods。这意味着做出一些权衡,其中一个就是CocoaPods创建头文件搜索路径的方式。CocoaPods允许任何pod以无命名空间的方式引用其他pod。 例如,pod B可能有某个文件包含有#import“A.h”这样的代码,CocoaPods将创建构建设置以允许此类导入成功。 然而,如果您尝试将module maps添加到pods中,这种导入方式就会失败。 多年前我们曾尝试为静态库自动生成module maps,这破坏了一些pod,导致我们不得不放弃。 在CocoaPods1.5.0版本中,您将能够选择更严格的头文件搜索路径(以及为Objective-C pods生成module maps)。

实际过程中,发现在Podfile中使用了use_modular_headers! 的结果是很多以前有效的Pod间的头文件引用会在编译期间报错。为解决这个问题,我们依次进行了以下几步:

  1. 对于没有Swift代码的业务组件,在Podfile中对其指定:modular_headers => false,这样可以避免一些编译错误,减少迁移的工作量。
  2. 修改pod中由于启用了modular headers产生的编译错误:
    eg. 在业务库pod A中有文件A.m,A.m因需要使用基础库pod B中的一些类而使用了@import B,同时又引了自身pod的一个文件AA.h,在AA.h中又有 @import B或者 #import<B/B.h>这样的代码。这将导致A.m中产生类似ambiguous reference的编译错误。解决方法也比较简单,通过删除一些重复引用来解除引用模糊就好了,但是由于涉及到的pods和文件比较多,花费了我们很多时间。
  3. 对于含有Swift代码的pod,所依赖的Objective-C的pod,无论私有pod或第三方pod都必须启用modular headers。否则会在pod install或update的时候报错。
The Swift pod `xxx` depends upon `aaa`, `bbb`, `ccc`, which do not define modules. To opt into those
targets generating module maps (which is necessary to import them from Swift when building as 
static libraries), you may set `use_modular_headers!` globally in your Podfile, 
or specify `:modular_headers => true` for particular dependencies.

完蛋,如果要对第三库也进行相关的修改,那将是一个浩大的工程。stack overflow上也有人遇到了相同的问题。无奈,我们只好暂时中止整个迁移过程。

阶段2.春天真的来了

之后,我一直保持着对Cocoapods版本更新的关注,终于盼到了1.6.0Beta。这个版本Cocoapods做了很多性能和稳定性方面的提升,大家可以去官网一览究竟,当然最好自己试一下,pod update快了很多。最引起我们注意的是在它的release notes里面发现了下面这条关于第三方库的描述:

When integrating a vendored framework while building pods as static libraries, public headers will be found via FRAMEWORK_SEARCH_PATHS instead of via the sandbox headers store.

简单说就是在把pods构建为static librarries时如果集成了第三方的framework,公开的头文件将通过 FRAMEWORK_SEARCH_PATHS来进行搜寻,而不是之前的沙盒内的头文件仓库。

然后我们重演了阶段一的那些流程,使用最新的1.6.0Beta尝试将我们的pods构建成static libraries.果然,这次第三方库没有报错。pod install成功了,编译通过了,pods的构建产物由之前的行李箱(xxx.framwork)变成了我们想要的小房子(xxx.a)O(∩_∩)O哈哈~。但是,我知道我们离成功还差一步--资源引用问题。

  • 资源引用
    iOS中动态库和静态库对资源的管理方式有着显著不同。这就导致我们需要在代码中对图片、localizedString等进行引用的时候也需要做一番更改。以下两点是基于我们在podspec中使用了resource_bundles来进行资源管理。

    • 对于动态库, 图片、.strings文件等资源会放在独立的bundle中,存储在自己所属的pod的最终产物framework下,因此通过main bundle是无法获取的。代码中引用资源时需要先传入本pod的一个class,通过+ (NSBundle *)bundleForClass:(Class)aClass拿到一个bundle对象,取得此的bundleName后再通过- (NSURL *)URLForResource:(NSString *)name withExtension:(NSString *)ext;获取bundle的URL,最终通过+ (instancetype)bundleWithURL:(NSURL *)url; 获取目的bundle。实际项目中我们对上面的系列操作在基础组件中封装了若干个宏,方便业务方调用。

    • 对于静态库则相对简单。资源最终都会以独立的bundle集成到main bundle,代码中需要在mainbundle中根据bundleName找到pod对应的bundle,然后取相应的资源就可以了。基本会有以下的代码:

    NSURL *bundleUrl = [[NSBundle mainBundle] URLForResource:bundleName withExtension:@"bundle"];
    NSBundle  *bundle = [NSBundle bundleWithURL:bundleUrl];
    UIImage *image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
    

终于写完了,总的看来其实也不复杂,主要的地方其实在于对CocoaPods新特性的使用,还有就是动态库与静态库资源引用方式不同的理解与适配。水平有限,如果有错误或者描述不清楚的地方,欢迎与我交流。