Swift Static Libraries遷移實踐

杭州鳳梨發表於2018-11-09

背景介紹:

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

相關文章