Swift Static Libraries遷移實踐

背景介紹:

二維火雲收銀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。git

動態庫與靜態庫的區別、優劣不在本文討論範圍以內,相關的文章網上能夠搜到不少。本文主要記錄了咱們爲何要轉向轉向Swift Static Libraries ,以及遷移過程遇到的一些問題和思考。github

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沒有正確設置,在這裏再次感謝一下文章做者。bash

  • 公司其餘業務線大多使用的是Static Libraries,不論是出於代碼複用或者產品需求,想要使用他們的SDK也會遇到第一個問題。要求他們同時提供和維護動態庫版本也不太現實,這就產生了咱們與其餘業務線在技術棧上的一個差別。

遷移過程

階段一. 春天來了

當看到CocoaPods官宣「CocoaPods 1.5.0 — Swift Static Libraries」時,咱們欣喜的覺得遷移Static libraries的時機終於到了。
官宣官宣:post

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」。ui

感受咱們的春天終於來了,因而趁着項目緩衝的功夫,召集你們搞起!搞起!this

遷移步驟基本參照了CocoaPods的官方指導,具體有如下幾點:url

  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).spa

翻譯過來是:翻譯

當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新特性的使用,還有就是動態庫與靜態庫資源引用方式不一樣的理解與適配。水平有限,若是有錯誤或者描述不清楚的地方,歡迎與我交流。

相關文章
相關標籤/搜索