Categories
程式開發

Swift和Objective-C混編在有贊移動的實踐


一、概述

隨著Xcode 11、Swift 5.1 的正式發布,Swift 目前已經實現了ABI 穩定及模塊穩定,語法及實現也比以往更加成熟穩定,所以我們在微商城和零售等業務線中嘗試使用Swift 開發部分業務,並在二方庫中進行混編開發,在此我們將一些混編經驗分享出來。

二、現狀

同一工程內的混編,通常來講有兩種方式:

1、在宿主工程利用橋接文件(Bridging-Header.h)進行混編

  • Swift 訪問 Objective-C

    只需要在橋接文件中(Bridging-Header.h)中導入需要暴露給 Swift 模塊的 Objective-C 類,即可在 Swift 中訪問相應 Objective-C 的類和方法

  • Objective-C 訪問 Swift

    在 Objective-C 類中導入 ProductName-Swift.h,即可訪問 Swift 中暴露給 Objective-C 的類和方法

2、利用 cocoapods 包管理工具,進行二/三方庫混編

  • Swift 訪問 Objective-C

    用 Swift Module 系統,需要用到的 Objective-C 類用 import xxx 進行引用,即可在 Swift 中訪問相應的 Objective-C 的類和方法

  • Objective-C 訪問 Swift

    在 Objective-C 類中導入 ProductName-Swift.h,即可訪問 Swift 中暴露給 Objective-C 的類和方法

由於我們目前的業務比如商品模塊、消息模塊、資產模塊等都是利用cocoapods 進行模塊化管理,製作成了二方庫,供微商城、零售、精選等業務線使用,不建議在宿主工程直接使用Swift 文件進行業務開發,業務代碼應該放到相應的業務模塊中去,因此我們將Swift 代碼放入二/三方庫中,進行混編。

三、Module 系統

3.1 LLVM Module 系統

講到混編方案,就不得不提,蘋果在 2012 年 11 月提出 LLVM 的 Module 系統,簡單講就是用樹形的結構化的描述來取代以往 #include ,例如傳統的 #include 現在變成 importstd.io

這樣做的主要意義是:

  • 語義上完整描述了一個框架的作用
  • 提高編譯時的可擴展性,同一模塊只需編譯或導入一次,避免了頭文件的多次引用、解析
  • 減少碎片化,每個模塊只處理一次,環境的變化不會導致不一致

3.2 modulemap 文件

modulemap 文件就是對一個框架,一個庫的所有文件的結構化描述。默認文件名是 module.modulemap 關於 LLVM module系統更加詳細的內容,可以參考Clang 官方文檔

3.3 Swift Module

蘋果為 Swift 設計了 SwiftModule。 SwiftModule 可以將 Swift 解析後生成對應的 modulemap 和 umbrella.h 文件,SwiftModule 增加對編譯器版本的依賴,編譯產物與編譯器 和 Swift 版本有關。如果想要實現 Swift 和 Objective-C 的互相訪問,需要 Objective-C 庫,以及對應的 umbrella.h 和 modulemap 支持。其中動態庫 framework 是 Xcode 支持配置並生成 header,靜態庫 .a 需要自己編寫對應的 umbrella.h 和 modulemap。即庫之間無論何種語言實現,均需要封裝為 LLVM Module 來相互訪問。

LLVM Module 作為蘋果公司提出的特性,已經被Swift 完全採用,在其基礎上建立自己的模塊系統,當我們結合Cocoapods 的use_modular_headers! 配置將三方庫構建成靜態庫,或者use_frameworks! 配置將三方庫構建成動態庫時,在編譯產物中都會生成一個modulemap 和module umbrella.h 文件

Swift和Objective-C混編在有贊移動的實踐 1

可以在 Swift 文件這樣引用該模塊

Swift和Objective-C混編在有贊移動的實踐 1

3.4 use_ modular_ headers!

該特性是 Cocoapods 1.5.0 引入的配置,目的是為了滿足 Xcode 9 以後支持的 Swift Static Libraries ,將 Swift Pods 構建成為靜態庫

  • 如果你的 Swift Pod 依賴於 Objective-C,那麼你需要為這個 Objective-C 庫啟用 modular_headers
  • 對於 pod 開發者可以在 podtargetxcconfig 內添加 ’DEFINES_MODULE’=>‘YES’,對於使用者在 podfile 內添加 use_modular _headers!
  • 在 podspec 中通過 modular_headers => true 配置特定的 pod

可以參考Cocoapods 官方文檔

四、微商城架構調整

基於上面這些背景,微商城結合團隊規模和實踐,計劃使用殼工程和模塊同 git 倉庫的 Cocoapods development pod 來替代現有的子項目方式封裝模塊,模塊間依賴基於 podspec 和 podfile 中的配置進行管理。並且為了不中斷團隊工作和持續交付,實行 Long Term Evolution 長期演進的策略。有關 development pod 可以參考Cocoapods 官方文檔。

微商城項目初期:

所有模塊均依賴 common 模塊,同時所有模塊也依賴了 Cocoapods 的二/三方庫;在新架構中,common 被封裝為 development pod, 並在 podspec 中聲明依賴。

調整後,原有的子項目通過頭文件暴露的方式仍舊可以訪問和依賴,模塊間的 Router 和 BeeHive/Bifrost 模塊管理也都支持,即該過程對於需求開發團隊是無痛的。

最終所有的 development pod 通過 Podfile 集成進殼工程,同時 Podfile 中增加 use _modular_headers! ,要求 Cocoapods 使用靜態庫集成並生成對應 modulemap 等 support file。我們在周會上和大家同步瞭如何將原有的Xcode 子項目模塊遷移到development pod ,簡言之分為三個部分,聲明源碼,聲明資源文件,聲明依賴和其他配置,具體podspec 文檔可以參考Cocoapods 官方文檔。

最終整體架構如下所示:

Swift和Objective-C混編在有贊移動的實踐 3

在上述版本交付並合併到 master 後,經過完整測試,大家的開發體驗沒有改變。之後將業務模塊也拆分為 development pod ,單個業務模塊直接依賴 common pod。在遷移過程中,可以先依賴 common 以實現對二/三方庫的依賴。隨業務迭代,單業務 development pod 也逐漸理清自身真實的依賴,最終可以把自己的依賴寫入 podspec。

五、二方庫混編

關於二方庫混編方案,我們整個有贊移動業務都是用Cocoapods 來管理二/三方庫,聲明use _modular_headers! 將Swift pods 構建成靜態庫,目前已經在消息業務模塊中已經實踐成功,在線上的狀況穩定。在此總結了一些混編方案所能遇到的問題。

5.1 Framework targets 不支持 Bridging-Header

通常來講混編的時候需要在工程中創建Swift 文件時候,Xcode 會問詢是否創建Bridging-Header 文件,點擊是,系統會幫你創建一個Bridging-Header,你可以將需要引用的Objective-C 模塊的頭文件放在裡面,然後你可以在Swift 模塊用Objective-C 的類。但是編譯器是不允許在 Framework 中創建 Bridging-header,因此在二/三方庫中,我們不能使用橋接文件的方式進行混編 Objective-C 代碼的引用,需要用 Swift Module 進行模塊間的引用。

5.2 模塊引用

引用其他 Objective-C 二方庫需要增加命名空間(Namespace),否則會報錯找不到文件
Swift 的命名空間是以模塊劃分的,一個模塊表示一個命名空間。開發時,默認添加到主 target 的內容是同處於同一個命名空間的;如果用 Cocoapods 導入的第三方庫,是以一個單獨的 target 存在,不會存在命名衝突。但如果以源碼的方式導入工程,很可能發生命名衝突,所以為了安全起見,第三方庫都會使用命名空間這種方式來防止衝突。

5.3 C++ 混編

Objective-C 是 C++ 的超集,就如同 Objective-C 是 C 的超集,在OS X 上同時被 GCC 和 Clang 支持編譯,.mm 是 Objective-C++ 的默認後綴名,Xcode 的編譯器可以識別。在.mm 文件中,Objective-C 代碼和 C++ 代碼都可以正常編譯運行。在消息業務模塊中中引用了 WCDB 這個 Objective-C++ 的庫,因此在引用的時候要將引用到的 WCDB.h 頭文件中的類文件的 .h 改成 .mm。

5.4 鏈接錯誤

我們將上述工作做完後引入到宿主工程中,進行編譯的時候會出現鏈接錯誤,不要擔心,那是因為宿主工程中缺少Swift 的某些系統庫,在宿主工程中建立一個Swift 文件方可解決。

5.5 Swift 調用 Objective-C

將 Swift 模塊文件中,用import xxx 的形式進行模塊的引用,包括 Objective-C 的二/三方庫

5.6 Objective-C 調用 Swift

  • Swift 類中將需要暴露給 Objective-C 模塊引用的類,用 public 申明
  • Swift 類中需要暴露給 Objective-C 的方法要用關鍵字 @objc
  • 在 Objective-C 類中引用 ProductName-Swift.h 頭文件即可引用暴露給 Objective-C 的 Swift 的類和方法

5.7 pod spec lint 驗證和發布

在 pod spec lint 驗證和 pod repo push 發布命令中增加 –use–modular-headers 關鍵字,否則驗證發布不通過

以上是在二方庫混編中遇到的一些問題,以供大家參考和探討。

六、優勢

  • Swift中二進制庫的數量逐年攀升,直到iOS13 已經有141個,Foundation 中的許多系統類已經由 Swift 庫實現
  • ABI 穩定,(iOS12.2系統以上)不增大包體積
  • Cocoapods 聲明 use_modular_headers! 構建 Swift 靜態庫,不影響啟動速度

七、總結

目前微商城項目已經進行了混編項目開發,比如學習中心模塊是一個純Swift 的二方庫,而消息業務模塊則是一個Swift 和Objective-C 混編的二方庫,我們後面會進行越來越多的模塊開髮用混編的這種形式,新的模塊採用Swift 代碼,老的業務還是Objective-C 不動這種方案。隨著Swift 越來越主流,很多大廠的App 都用該語言進行開發,但是不能一蹴而就全部將Objective-C 轉成Swift,而是有很長一段時間都是混編的形式存在,希望該篇文章能夠對想進行混編方案的開發者提供一定的參考。

參考文獻:

  • Swift 官方文檔:

https://swift.org/blog/swift-5-released/

  • Clang 官方文檔:

https://clang.llvm.org/dObjective-Cs/Modules.html

  • CocoaPods 官方文檔:

https://guides.cocoapods.org/making/making-a-cocoapod.html

本文轉載自公眾號有贊coder(ID:youzan_coder)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzAxOTY5MDMxNA==&mid=2455760552&idx=1&sn=0e8fcd64a9e6ca663de8c5ac427b7ed4&chksm=8c68688dbb1fe19bb64cb5a3f53bd3311d7b7c5825d9e988a7daeb3fa6daae02718277fcb19f&scene=27#wechat_redirect