- 本文首發于: ShannonChenCHN/ASwiftTour
- 源代碼地址:ShannonChenCHN/ASwiftTour
關鍵詞:模塊化/組件化、ObjC-Swift 混編、Swift 靜態庫、ABI Stability、Module Stability、LLVM Module、Umbrella Header
目錄
- 基礎準備工作
- 在一個 App Target 內部混編
- 在一個 Framework Target 中混編
- 踩坑之旅
- 項目背景
- 靜態庫子工程的集成
- 靜態鏈接問題
- 動態鏈接問題
- ABI Stability 和
Always Embed Swift Standard Library
選項 - 當模塊化/組件化項目遇到 Swift 靜態庫
- ObjC 模塊調用 Swift 模塊
- Swift 模塊調用 Swift 模塊
- Module Stability
- Swift 模塊調用 ObjC 模塊
- LLVM Module 和 Umbrella Header
- 調試問題
- 總結
一、基礎準備工作
在正式開始實踐 Swift-ObjC 混編之前,我們有一些問題是繞不過去的,比如:
- Swift 和 ObjC 混編,我們怎么開始?官方文檔有相關的介紹嗎?
- 在模塊化/組件化的項目中,Swift 和 ObjC 怎么混編?
- 業界中已經開始 Swift-ObjC 混編的項目,他們是怎么做的?
- 我們的現狀如何,針對這些已有的經驗需要做哪些考量?我們應該怎么做?
- 如果在現有的 ObjC 項目中引入 Swift,會帶來哪些影響?在哪些方面會有限制?
- ...
在 Apple 的官方文檔中有關于 Language Interoperability 的詳細介紹,主要是從 ObjC 遷移到 Swift 的角度來描述的,總結下來主要是以下三點:
- 如何調整項目中現有的 ObjC 和 C 代碼的 API,以提供給 Swift 調用,比如添加 nullability 相關的宏和關鍵字,添加 Swift API 別名等等
- 各種基礎數據類型在 Cocoa Framework 與 Swift 之間的轉換關系
- 如何在 Swift 代碼中調用 ObjC 代碼,以及如何在 ObjC 代碼中調用 Swift 代碼
這里我們重點關注的是如何實現 Swift 代碼和 ObjC 代碼的相互調用。
1. 在一個 App Target 內部混編
如果是在一個 App Target 內部混編的話,當我們在 ObjC 項目中新建 Swift 文件時或者在 Swift 項目中新建 ObjC 文件時,Xcode 都會自動幫你新建一個 Objective-C bridging header file
(當然我們也可以手動創建),我們可以在這個文件中導入需要暴露給 Swift 代碼調用的 ObjC 頭文件,這樣我們就能在 Swift 中調用 ObjC 的代碼了。
圖 1 Objective-C bridging header 文件的創建
圖 2 在 Swift 中調用 ObjC 的代碼
如果我們想在 ObjC 代碼中調用 Swift 的代碼,只需要寫上一行 import "ProductModuleName-Swift.h"
(這里的 ProductModuleName
表示 target 的名字)就可以了,因為在編譯時,編譯器會自動給項目中的 Swift 代碼生成一個 ProductModuleName-Swift.h
的頭文件(這個文件是編譯產物,我們在 build 目錄可以看到它),暴露給 ObjC 使用。
[圖片上傳失敗...(image-27dc28-1591538276949)]
圖 3 在 ObjC 中調用 Swift 的代碼
2. 在一個 Framework Target 中混編
除了在一個 App Target 內部混編之外,還有一種情況是當我們要寫一個 Library 或者 Framework 給別人用時,這個時候如果有 ObjC 和 Swift 的混編,Objective-C bridging header 的方式已經不適用了,如果我們用了這個頭文件,Xcode 在預編譯時也會警告我們。
先來看看 Swift 怎么調用 ObjC,正確的做法是將 Build Settings 中的 Defines Module 選項設置為 YES
,
然后新建一個 umbrella header,再將需要暴露給(內部的) Swift 調用的 ObjC 的頭文件在這個 umbrella header 中導入(LLVM Module 和 umbrella header 是兩個新概念,后面會做具體介紹)。
如果要想在 ObjC 調用 Swift,同樣也要將 Build Settings 中的 Defines Module 選項設置為 YES
,然后在要引用 Swift 代碼的 ObjC 文件中導入編譯器生成的頭文件 #import <ProductName/ProductModuleName-Swift.h>
。
參考
二、踩坑之旅
1. 項目背景
--------------------------------------------------
Hotel | HotelChat | ... 業務層
--------------------------------------------------
HotelFoundation | HotelContracts | ... 業務基礎層
--------------------------------------------------
Network | Foundation | MapKit | RNKit | ... 基礎框架層
--------------------------------------------------
圖 4 筆者所在公司的 iOS 客戶端架構示意圖
目前筆者所在公司的項目整體架構是采用模塊化設計的,而且整個項目完全都是使用 ObjC/C 實現的,在實際開發時,各模塊既可以以源碼的形式使用,也可以以.a + .h + 資源 bundle
的形式使用,簡而言之,既可以源碼依賴,也可以是靜態庫依賴。那么我們可以直接在項目中使用 Swift 靜態庫嗎?
[圖片上傳失敗...(image-ab6e71-1591538276949)]
圖 5 項目結構示意圖(簡化模型)
我們都知道,從 Xcode 9 開始,Apple 就開始支持 Swift 靜態庫的使用了,所以我們現有的項目架構并不需要調整,引入 Swift 代碼的話是可以以靜態庫的形式出現的。
2. 靜態庫子工程的集成
我們要做的第一步,就是創建一個 Swift 靜態庫工程,然后再把它作為子工程集成到 ObjC 主工程中去。
大概的步驟如下:
- 創建 Swift 靜態庫工程(這里我們給它取個名字,叫
SwiftLibA
,主工程叫MainProject
) - 在主工程中集成 Swift 靜態庫工程
- 添加子工程:將 Swift 靜態庫工程的 xcodeproj 文件拖到主工程中
- 添加構建依賴:在 Build Phases 面板的
Dependencies
中添加這個靜態庫的 target 為構建依賴 - 添加要鏈接的靜態庫:在 Build Phases 面板的
Link Binary With Libraries
中鏈接這個 Swift 靜態庫 - 導出
xxx-Swift
頭文件:在 Swift 靜態庫工程的Run Script Phase
中添加腳本,將編譯器生成的SwiftLibA-Swift
頭文件復制到 build 目錄下(如圖 6 所示)
- 在 ObjC 代碼中調用 Swift API
- 在 Swift 代碼中添加
@objc
、public
等關鍵字 - 在 ObjC 代碼中添加
#import <SwiftLibA/SwiftLibA-Swift.h>
(這里的 SwiftLibA 是新添加的靜態庫的名字)
- 在 Swift 代碼中添加
[圖片上傳失敗...(image-d785b0-1591538276949)]
圖 6 在主工程中集成 Swift 靜態庫工程
[圖片上傳失敗...(image-50de62-1591538276949)]
圖 7 復制 xxx-Swift
頭文件到 build 目錄下
示例代碼:
@objcMembers
public class SwiftLibA: NSObject {
public func sayHello() {
print("Hello, this is Swift world!")
}
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[SwiftLibA new] sayHello];
}
@end
問題:
1.為什么需要設置 Dependencies
?
設置 Dependencies
是為了告訴 Xcode build system 在編譯主工程之前,需要先編譯哪些其他的 target,簡而言之,就是編譯依賴。
2.為什么需要設置 Link Binary With Libraries
?
Xcode 在 build 主工程時,會先編譯好各個子工程,最后再鏈接成一個可執行文件,通過這個 Link Binary With Libraries
設置,我們可以指定需要參與鏈接的靜態庫。
3.為什么需要復制 xxx-Swift
頭文件到 build 目錄下?
因為編譯時自動生成的頭文件是在 Intermediates
目錄中各子工程所屬的 DerivedSources
中,比如在我的電腦上就是 /Users/ShannonChen/Library/Developer/Xcode/DerivedData/MainProject-aptbbpsumoitdlhbzjckyglkspoi/Build/Intermediates.noindex/SwiftLibA.build/Debug-iphonesimulator/SwiftLibA.build/DerivedSources/SwiftLibA-Swift.h
,而主工程在編譯時會到 Build
目錄下的 Products
目錄去找頭文件,在我的電腦上就是 /Users/ShannonChen/Library/Developer/Xcode/DerivedData/MainProject-aptbbpsumoitdlhbzjckyglkspoi/Build/Products/Debug-iphonesimulator/include
,所以主工程或者其他子工程在編譯時就找不到這個頭文件了。
因此,我們就需要把這個 xxx-Swift
頭文件復制到 build 目錄下,具體腳本內容如下:
# 將編譯器生成的 xxx-Swift 頭文件拷貝到 build 目錄下的 include 目錄中
include_dir=${BUILT_PRODUCTS_DIR}/include/${PRODUCT_MODULE_NAME}/
mkdir -p ${include_dir}
cp ${DERIVED_SOURCES_DIR}/*-Swift.h ${include_dir}
參考:
3. 靜態鏈接問題
集成好 Swift 靜態庫之后,我們再 build 一下,發現在鏈接時仍然會報錯。
[圖片上傳失敗...(image-eae23a-1591538276949)]
圖 8 靜態鏈接時報錯
根據報錯信息來看,是因為找不到 swiftFoundation
這些動態庫,這是由于我們的主工程是純 ObjC 項目,所以我們需要告訴 Xcode build system 這些 Swift 動態庫的路徑。
在 Build Settings
tab 下找到 Library Search Paths
,添加上 $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)
,另外還需要添加 Swift 5.0 的動態庫所在的路徑 $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
。
這兩個目錄都可以在我們的電腦上看到:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/iphoneos
[圖片上傳失敗...(image-60b25d-1591538276949)]
圖 9 Swift 標準庫
參考:
4. 動態鏈接問題
靜態鏈接的問題已經解決了,此時按下 ?+R
,模擬器啟動后發生崩潰。控制臺上的日志信息顯示 dyld: Library not loaded: @rpath/libswiftCore.dylib
,這是因為程序啟動時 Swift 動態庫加載失敗了。
[圖片上傳失敗...(image-7b784d-1591538276949)]
圖 10 程序啟動時發生崩潰
為了解決這個問題,我們需要設置兩個地方(只要你的項目 iOS Deployment Target
是 12.2 以下,這兩個就都需要設置):
- 針對 iOS 12.2 及以后的系統,需要在
Build Settings
tab 下的Runpath Search Path
中最前面添加/usr/lib/swift
。 - 針對 iOS 12.2 以前的系統,需要將
Build Settings
tab 下的Always Embed Swift Standard Libraries
設置為YES
。
為什么我們要分別針對 iOS 12.2 之前和之后的系統做不同的設置呢?將 Always Embed Swift Standard Libraries
設置為 YES
是不是意味著每次打包時都會把 Swift 標準庫打進去呢?
參考:
5. ABI Stability 和 Always Embed Swift Standard Library
選項
2019 年對 iOS 開發者來說,最大的新聞莫過于 Swift ABI 終于穩定了。ABI Stability 意味著什么呢?ABI Stability 也就是 binary 接口穩定,在運行的時候只要是用 Swift 5.0 或更高版本的編譯器(Swift 5.0 對應 Xcode 10.2)構建出來的 app,就可以跑在任意的 Swift 5.0 或更高版本的 Swift runtime 上了。這樣,我們就不需要像以往那樣每次打一個新的 app 時都要帶上一套 Swift runtime 和 standard library 了,iOS 和 macOS 系統里就會內置一套 Swift runtime 和 standard library。
圖 11 在這個例子中,基于 Swift 5.0 構建出來的 app 可以直接在內置了 Swift 5 或者 Swift 5.1,甚至 Swift 6 標準庫的系統上運行
但是如果你用的是 Swift 5.0 以前版本的編譯器,那么打包時還是會帶上一套 Swift runtime 和 standard library。
另外,對于用 Swift 5.0 或更高版本的編譯器構建出來的 app,在發布 app 時,Apple 將根據 iOS 系統創建不同的下載包。對于 iOS 12.2 及以上的系統,因為系統內置了 Swift 5 的 runtime 和 standard library,所以 app 中不再需要嵌入 Swift 的庫,它們會被從 app bundle 中刪掉。但是對于 iOS 12.2 以下的系統,因為系統中沒有內置 Swift 5 的 runtime 和 standard library,所以打包時仍然需要帶上。
理解了什么是 ABI Stability,就好理解我們前面在 Build Settings
所做的兩個設置了。
app 在啟動/運行時,會先看 app bundle 中有沒有 Swift runtime,如果找不到,動態鏈接器 dyld
會到 runpath
路徑下查找 dylib(這個 runpath
路徑是一個系統目錄路徑)。所以我們針對 iOS 12.2 及以后的系統添加了 Runpath Search Path:/usr/lib/swift
,針對 iOS 12.2 以前的系統設置了 Always Embed Swift Standard Library
。
[圖片上傳失敗...(image-aee031-1591538276949)]
圖 12 添加 Runpath Search Path
Always Embed Swift Standard Library
曾經叫做 Embedded Content Contains Swift Code
,字面上看上去像是“總是嵌入 Swift 標準庫”,但是實際上這里只是告訴 build system 要做的事,并不代表用戶手機上下載的 app 是這樣的,因為在發布 app 時,app thinning 會自動根據目標系統來決定是否將 app bundle 中的 Swift 標準庫刪掉。
[圖片上傳失敗...(image-bceda8-1591538276949)]
圖 13 app thinning 會自動根據目標系統來決定是否將 app bundle 中的 Swift 標準庫刪掉
那么這個 Always Embed Swift Standard Library
是用來告訴 build system 做什么的呢?只要你的 target 會引用到 Swift 文件或者庫,就需要把它設置為 YES
,比如我們這里的主工程用到了 Swift 靜態庫,所以就需要設置為 YES
,還有一種情況是你的 target 是一個測試工程,但是引用了 Swift 代碼,那么也需要設置為 YES
。另外,筆者試驗了一下,如果給一個純 ObjC 的項目中添加了一個 Swift 文件,Xcode 會自動將這個選項設置為 YES
。
[圖片上傳失敗...(image-dfb2c3-1591538276949)]
圖 14 設置 Always Embed Swift Standard Library
參考:
- https://stackoverflow.com/a/55365318
- https://stackoverflow.com/a/26949219
- https://onevcat.com/2019/02/swift-abi/
- https://swift.org/blog/abi-stability-and-more/
- https://swift.org/blog/abi-stability-and-apple/
- https://developer.apple.com/documentation/xcode_release_notes/xcode_10_2_release_notes/swift_5_release_notes_for_xcode_10_2
6. 模塊化/組件化
前面提到過,筆者所在公司 iOS 項目是采用的是模塊化架構,而模塊之間是有依賴關系的。一般是上層模塊依賴于下層的模塊,如圖 4 所示。
這里先說明一下我們這里所說的模塊的概念,在我們的項目中,一個 ObjC 模塊就是 .a 靜態庫 + .h 頭文件 + bundle 資源文件
的組合。
6.1 ObjC 模塊調用 Swift 模塊
如前面所說,ObjC 調用 Swift 代碼時,只需要導入編譯 Swift 模塊時自動生成的頭文件 xxx-Swift.h
就可以了。
比如,模塊 ObjCLibA 調用模塊 SwiftLibA:
#import "ObjCLibA.h"
#import <SwiftLibA/SwiftLibA-Swift.h>
@implementation ObjCLibA
- (void)sayHello {
[[SwiftLibA new] sayHelloWithName:@"ObjCLibA"];
}
@end
這樣的確沒問題,但是考慮到持續持續交付平臺上各個模塊都是獨立編譯的情況,像上面的這個例子中,如果單獨編譯模塊 ObjCLibA 的話,就會出現頭文件找不到的錯誤: 'SwiftLibA/SwiftLibA-Swift.h' file not found
。
[圖片上傳失敗...(image-42aa5e-1591538276949)]
圖 15 模塊 ObjCLibA 調用模塊 SwiftLibA,(a)編譯主工程沒問題,(b)但是單獨編譯模塊 ObjCLibA 就報錯了
這是因為 SwiftLibA-Swift.h 文件是編譯模塊 SwiftLibA 時的產物,是生成在 build 目錄中,而不是工程代碼所在的目錄中。這一點我們在前面已經討論過,這里不再贅述。
我們都知道使用 #import
指令導入頭文件有兩種形式,#import "xxx.h"
和 #import <xxx.h>
,編譯器在編譯 ObjC 代碼時會根據不同的指令形式去搜索頭文件,對于前者來說是到項目(源代碼)所在目錄下搜索,對于后者是到環境變量所指的目錄或者指定目錄下去搜索的。
所以要想解決這個問題,我們可以換個思路,這個 SwiftLibA-Swift.h 文件是根據我們寫的 Swift 代碼公有 API 生成的,那么我們每次修改 Swift 代碼的公有 API 時,它就會更新一次,所以,我們可以在每次 build 這個模塊時把最新生成的拷貝到源碼所在目錄下(這個文件需要加入到版本控制中和其他代碼一起提交),然后再把新的路徑添加到 ObjC 模塊的 Header Search Path
中,另外,ObjC 模塊中頭文件導入的方式也要改成雙引號的形式。
完整腳本如下:
generated_header_file=${DERIVED_SOURCES_DIR}/*-Swift.h
include_dir=${BUILT_PRODUCTS_DIR}/include/${PRODUCT_MODULE_NAME}/
# 將編譯器生成的 xxx-Swift 頭文件拷貝到 build 目錄下的 include 目錄中
mkdir -p ${include_dir}
cp ${generated_header_file} ${include_dir}
# 去掉 xxx-Swift.h 文件頭部注釋中的編譯器的版本號
sed -i "" "s/^\/\/ Generated by Apple.*$/\/\/ Generated by Apple/g" ${generated_header_file}
# 拷貝 xxx-Swift.h 文件到工程源碼目錄
header_file_in_proj=${SRCROOT}/${PROJECT}-Swift.h
needs_copy=true
if [ -f "$header_file_in_proj" ]; then
echo "${header_file_in_proj} 已存在"
new_content=$(cat ${generated_header_file})
old_content=$(cat ${header_file_in_proj})
if [ "$new_content" = "$old_content" ];then
echo "文件內容一致,無需再Copy:"
echo "${generated_header_file} "
echo "${header_file_in_proj} "
needs_copy=false
fi
fi
if [ "$needs_copy" = true ] ; then
echo "文件內容不一致,需要Copy:"
echo "復制文件: "
echo "${generated_header_file} "
echo "${header_file_in_proj} "
cp ${generated_header_file} ${header_file_in_proj}
fi
[圖片上傳失敗...(image-aa20de-1591538276949)]
圖 16 將編譯器生成的頭文件拷貝到源代碼目錄
參考:
6.2 Swift 模塊調用 Swift 模塊
ObjC 模塊調用 Swift 模塊的問題解決了,那么如果 Swift 模塊調用 Swift 模塊呢?會不會也存在類似的問題?
先來看一個例子,還是前面的那個示例項目,只不過多了一個模塊 SwiftLibB:
- MainProject
- ObjCLibA
- SwiftLibA
- SwiftLibB
然后我們在模塊 SwiftLibA 中調用了模塊 SwiftLibB 中的 API:
import Foundation
import SwiftLibB
@objcMembers
public class SwiftLibA: NSObject {
public func sayHello(name: String) {
SwiftLibB().sayHello(name: name)
print("Hello, this is " + name + "!")
print("-- Printed by SwiftLibA")
}
}
這個時候如果編譯主工程是沒問題的,但是如果單獨編譯模塊 SwiftLibA 就會報錯:No such module 'SwiftLibB'
。
[圖片上傳失敗...(image-9247f6-1591538276949)]
圖 17 模塊 SwiftLibA 調用模塊 SwiftLibB,(a)編譯主工程沒問題,(b)但是單獨編譯模塊 SwiftLibA 就報錯了
這個問題看上去跟前面遇到的 ObjC 模塊調用 Swift 模塊的問題是一樣的,但是我們要知道 Swift 中是沒有頭文件的概念的,那么 Swift 是通過什么方式暴露公開 API 的呢?
不同于 C-based 語言使用 manually-written 頭文件來提供公開接口,Swift 是通過一個叫做 swiftmodule
的文件來描述一個 library 的 interface,這個 swiftmodule
文件是編譯器自動生成的。我們打開 SwiftLibB 模塊的 build 目錄,可以看到編譯器自動生成的 SwiftLibB.swiftmodule
,這個 SwiftLibB.swiftmodule
目錄下有兩種文件:swiftmodule
文件和 swiftdoc
文件。swiftmodule
文件和 swiftdoc
文件都是二進制文件,我們可以用反編譯工具查看其中的內容,swiftmodule
文件里面保存了模塊的信息,而 swiftdoc
文件則保存了源代碼中的注釋內容。
[圖片上傳失敗...(image-439026-1591538276949)]
圖 18 build 目錄下的 swiftmodule 文件
看到這里,你可能會想我們只要像導出 xxx-Swift.h
文件一樣,把這幾個 swiftmodule
文件導出到源代碼目錄,然后再設置 SwiftLibA 的 import path
,另外再把這幾個文件加入 git 版本控制中就解決了。
是的,我一開始也是這么想的,然后我就這么去做了,單獨編譯 SwiftLibA 確實問題,但是提交到 git 遠程倉庫之后,持續交付平臺上的 SwiftLibA 模塊卻編譯報錯了:
... error:
Module compiled with Swift 5.1 cannot be imported by the Swift 5.1.2 compiler
...
Module Stability
上面的方法之所以行不通,是因為 swiftmodule
文件跟編譯器版本是綁定的,在 Swift 5.0 之前,Apple 官方沒有提供解決辦法,在發布 Swift 5.0 時,除了 ABI Stability 之外,Apple 還解決了一個重要的,就是 Module Stability,也就是我們這里遇到的問題。
ABI Stability 解決的是不同 Swift 版本的代碼在運行時的兼容性問題,而 Module Stability 則要解決的是不同 Swift 版本的代碼在編譯時的兼容性問題。具體介紹可以看一下 Swift 官方博客 ABI Stability and More 和 WWDC 2019 的視頻 Binary Frameworks in Swift,以及社區的討論 Plan for module stability、Update on Module Stability and Module Interface Files。
[圖片上傳失敗...(image-8dfb12-1591538276949)]
圖 19 swift.org 官方博客上關于 Module Stability 的介紹
針對 Module Stability,Apple 提供的解決方案是 swiftinterface
文件,swiftinterface
文件是作為 swiftmodule
的一個補充,它是一個描述 module 公開接口的文本文件,不受編譯器版本限制。比如,你用 Swift 5.0 的編譯器編譯出了一個 library,它的 swiftinterface
文件可以在 Swift 5.1 的編譯器上使用。
我們現在打開 SwiftLibB 的 Build Setting
,找到 Build Options -> Build Libraries for Distribution
,把它設置為 YES
,重新編譯一下,再看看 build 目錄中生成的 SwiftLibB.swiftmodule
,里面多了幾個 swiftinterface
文件。
[圖片上傳失敗...(image-78509f-1591538276950)]
圖 20 Build Libraries for Distribution
選項
[圖片上傳失敗...(image-4ea1c5-1591538276950)]
圖 21 編譯器自動生成的 swiftinterface
文件
我們可以打開 swiftinterface
文件跟源代碼對一下,它其實就是一個 swift 頭文件。
源代碼:
import Foundation
@objcMembers
public class SwiftLibB: NSObject {
public func sayHello(name: String) {
print("Hello, this is " + name + "!")
print("-- Printed by SwiftLibB")
}
}
swiftinterface
文件中的內容:
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7)
// swift-module-flags: -target x86_64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -module-name SwiftLibB
import Foundation
import Swift
@objc @objcMembers public class SwiftLibB : ObjectiveC.NSObject {
@objc public func sayHello(name: Swift.String)
@objc override dynamic public init()
@objc deinit
}
為了能夠滿足模塊 SwiftLibA 的單獨編譯,跟前面對 xx-Swift.h
文件的操作一樣,我們用腳本把 SwiftLibB.swiftmodule
拷貝到源代碼目錄中,然后再把這個新路徑添加到 SwiftLibA 的 Build Setting -> Swift Compiler-Search Paths -> Import Paths
中。
[圖片上傳失敗...(image-da225b-1591538276950)]
圖 22 添加 swiftmodule 文件的路徑到 SwiftLibA 的 import paths
這個方案對于模塊化/組件化有個缺點就是,每次編譯 Swift 模塊時需要考慮多種不同的 CPU 架構。
除了這個方案之外,還有其他兩個方案可以解決 Swift 模塊之間依賴的問題:
- 我們還可以把 SwiftLibB 作為 SwiftLibA 的子工程(因為
xcodeproj
文件可以作為 reference 引用),然后再設置編譯依賴,但不設置Link Binary With Libraries
,這樣就能保證 SwiftLibA 編譯通過,但是不會重復鏈接。 - 在 Swift 5 正式發布之前,還不支持 Module Stability,有 Swift 開發者用 ObjC 把 Swift 包一層,然后 ObjC 頭文件作為公開接口(詳見 Swift 5 Module Stability Workaround for Binary Frameworks)
參考
- https://medium.com/better-programming/create-swift-5-static-library-f1c7a1be3e45
- http://andelf.github.io/blog/2014/06/19/modules-for-swift/
- https://stackoverflow.com/a/58656323
- https://swift.org/blog/abi-stability-and-more/
- https://forums.swift.org/t/plan-for-module-stability/14551
- https://forums.swift.org/t/update-on-module-stability-and-module-interface-files/23337
- https://developer.apple.com/videos/play/wwdc2019/416/
- https://instabug.com/blog/swift-5-module-stability-workaround-for-binary-frameworks/
- https://medium.com/swiftify/swift-5-1-module-format-stability-best-time-migrate-objective-c-frameworks-a0434f5352a3
6.3 Swift 模塊調用 ObjC 模塊
如果是在同一個 app target 里,Swift 調用 ObjC 可以通過 Objective-C bridging header 來實現,但是如果是跨模塊的調用呢?Swift 模塊怎么調用 ObjC 模塊?
根據 Apple 官方文檔中的介紹,在 Library 或者 Framework 中不能使用 bridging header 的,而應該使用 umbrella header。
LLVM Module 和 Umbrella Header
什么是 umbrella header?這就涉及到了 LLVM Module 的概念,LLVM 引入 Module 是為了解決傳統的 #include
和 #import
這些頭文件導入機制所存在的問題,也就是說這是一種新的頭文件管理機制,LLVM 官方文檔中對此有詳細的介紹。
在 ObjC 中可以通過 @import
指令導入 module,在 Swift 中通過 import
關鍵字導入 module。
Module 機制中一個很重要的文件就是 module map 文件,module map 文件是用來描述頭文件和 module 結構的在邏輯上的對應關系的。
The crucial link between modules and headers is described by a module map, which describes how a collection of existing headers maps on to the (logical) structure of a module. For example, one could imagine a module
std
covering the C standard library. Each of the C standard library headers (stdio.h
,stdlib.h
,math.h
, etc.) would contribute to thestd
module, by placing their respective APIs into the corresponding submodule (std.io
,std.lib
,std.math
, etc.). Having a list of the headers that are part of thestd
module allows the compiler to build thestd
module as a standalone entity, and having the mapping from header names to (sub)modules allows the automatic translation of#include
directives to module imports.Module maps are specified as separate files (each named
module.modulemap
) alongside the headers they describe, which allows them to be added to existing software libraries without having to change the library headers themselves (in most cases [2]).
每一個 library 都會有一個對應的 module.modulemap
文件,這個文件中會聲明要引用的頭文件,這些頭文件就跟 module.modulemap
文件放在一起。
The module map language describes the mapping from header files to the logical structure of modules. To enable support for using a library as a module, one must write a
module.modulemap
file for that library. Themodule.modulemap
file is placed alongside the header files themselves, and is written in the module map language described below.
一個 C 標準庫的 module map 文件可能就是這樣的:
module std [system] [extern_c] {
module assert {
textual header "assert.h"
header "bits/assert-decls.h"
export *
}
module complex {
header "complex.h"
export *
}
module ctype {
header "ctype.h"
export *
}
module errno {
header "errno.h"
header "sys/errno.h"
export *
}
module fenv {
header "fenv.h"
export *
}
// ...more headers follow...
}
modulemap 中的內容是使用 module map 語言來實現的,module map 語言中有一些保留字,其中 umbrella
就是用來聲明 umbrella header 的。umbrella header 可以把所在目錄下的所有的頭文件都包含進來,這樣開發者中只要導入一次就可以使用這個 library 的所有 API 了。
A header with the
umbrella
specifier is called an umbrella header. An umbrella header includes all of the headers within its directory (and any subdirectories), and is typically used (in the#include
world) to easily access the full API provided by a particular library. With modules, an umbrella header is a convenient shortcut that eliminates the need to write outheader
declarations for every library header. A given directory can only contain a single umbrella header.
如果你創建的是 Framework,在創建這個 Framework 時,defines module
默認會設置為 YES
,編譯這個 Framework 之后,可以在 build 目錄下看到自動生成的 Module
目錄,這個 Module
目錄下有自動創建的 modulemap
文件,其中引用了自動創建的 umbrella header。但是如果你創建的是 static library,那就需要開發者手動為這個 module 創建 modulemap
文件和要引用的 umbrella header。
接下來我們創建一個 ObjCLibB 模塊,然后讓 SwiftLibA 模塊來調用它。
首先要做的是給模塊 ObjCLibB 新建一個 umbrella header 文件和一個 modulemap
文件,然后再把 modulemap 文件的路徑添加到 SwiftLibA 的 import paths,把 umbrella header 文件的路徑添加到 SwiftLibA 的 header search paths,這樣就大功告成了。
[圖片上傳失敗...(image-3fe881-1591538276950)]
圖 23 新建 umbrella header 文件
[圖片上傳失敗...(image-eafcae-1591538276950)]
圖 24 新建 modulemap 文件
[圖片上傳失敗...(image-36d57d-1591538276950)]
圖 25 添加 modulemap 文件的路徑到 SwiftLibA 的 import paths
[圖片上傳失敗...(image-d2a0dc-1591538276950)]
圖 26 添加 umbrella header 文件的路徑到 SwiftLibA 的 header search paths
如果你的 Swift 模塊要調用的模塊是 ObjC-Swift 混編的,也可用同樣的方式來實現,核心點就在于將 C-based 語言的頭文件用 modulemap 和 umbrella header 封裝起來。
參考:
- https://samsymons.com/blog/understanding-objective-c-modules/
- https://medium.com/swift-and-ios-writing/using-a-c-library-inside-a-swift-framework-d041d7b701d9
- https://medium.com/allatoneplace/challenges-building-a-swift-framework-d882867c97f9
- https://clang.llvm.org/docs/Modules.html
- https://hechen.xyz/post/swift-and-modules/
7. 調試問題
如果你的主工程是純 ObjC 實現的,那么當你在斷點調試 Swift 模塊中的代碼時,會無法看到變量值,即便在 console 上使用 LLDB 命令也打印不出來。
(lldb) po name
Cannot create Swift scratch context (couldn't load the Swift stdlib)Cannot create Swift scratch context (couldn't load the Swift stdlib)Shared Swift state for MainProject could not be initialized.
The REPL and expressions are unavailable.
[圖片上傳失敗...(image-a4f8a4-1591538276950)]
圖 27 調試 Swift 代碼時無法看到變量值
這是因為主工程中沒有 Swift 代碼,所以就沒有 Swift 相關的環境和設置選項,解決辦法就是在主工程中創建一個新的 Swift 文件。
三、總結
Swift 5 的到來終于讓我們看到了期待已久的 ABI 穩定,相信更現代、更安全的 Swift 會變得越來越流行。另外,在模塊化/組件化項目中落地 Swift 時,LLVM Module 是一個繞不過去的話題,LLVM Module 改變了傳統 C-Based 語言的頭文件機制,取而代之的是 Module 的思維。技術的發展會帶來更先進的生產力,我們期待 Swift 在未來能夠進一步提升我們的開發效率和編程體驗。