如何在模塊化/組件化項目中實現 ObjC-Swift 混編?

關鍵詞:模塊化/組件化、ObjC-Swift 混編、Swift 靜態庫、ABI Stability、Module Stability、LLVM Module、Umbrella Headerhtml

目錄

  • 基礎準備工做
    • 在一個 App Target 內部混編
    • 在一個 Framework Target 中混編
  • 踩坑之旅
    • 項目背景
    • 靜態庫子工程的集成
    • 靜態連接問題
    • 動態連接問題
    • ABI Stability 和Always Embed Swift Standard Library 選項
    • 當模塊化/組件化項目遇到 Swift 靜態庫
      • ObjC 模塊調用 Swift 模塊
      • Swift 模塊調用 Swift 模塊
      • Module Stability
      • Swift 模塊調用 ObjC 模塊
      • LLVM Module 和 Umbrella Header
    • 調試問題
  • 總結

1、基礎準備工做

在正式開始實踐 Swift-ObjC 混編以前,咱們有一些問題是繞不過去的,好比:ios

  • Swift 和 ObjC 混編,咱們怎麼開始?官方文檔有相關的介紹嗎?
  • 在模塊化/組件化的項目中,Swift 和 ObjC 怎麼混編?
  • 業界中已經開始 Swift-ObjC 混編的項目,他們是怎麼作的?
  • 咱們的現狀如何,針對這些已有的經驗須要作哪些考量?咱們應該怎麼作?
  • 若是在現有的 ObjC 項目中引入 Swift,會帶來哪些影響?在哪些方面會有限制?
  • ...

在 Apple 的官方文檔中有關於 Language Interoperability 的詳細介紹,主要是從 ObjC 遷移到 Swift 的角度來描述的,總結下來主要是如下三點:git

  • 如何調整項目中現有的 ObjC 和 C 代碼的 API,以提供給 Swift 調用,好比添加 nullability 相關的宏和關鍵字,添加 Swift API 別名等等
  • 各類基礎數據類型在 Cocoa Framework 與 Swift 之間的轉換關係
  • 如何在 Swift 代碼中調用 ObjC 代碼,以及如何在 ObjC 代碼中調用 Swift 代碼

這裏咱們重點關注的是如何實現 Swift 代碼和 ObjC 代碼的相互調用github

1. 在一個 App Target 內部混編

若是是在一個 App Target 內部混編的話,當咱們在 ObjC 項目中新建 Swift 文件時或者在 Swift 項目中新建 ObjC 文件時,Xcode 都會自動幫你新建一個 Objective-C bridging header file(固然咱們也能夠手動建立),咱們能夠在這個文件中導入須要暴露給 Swift 代碼調用的 ObjC 頭文件,這樣咱們就能在 Swift 中調用 ObjC 的代碼了。objective-c

圖 1 Objective-C bridging header 文件的建立

圖 2 在 Swift 中調用 ObjC 的代碼

若是咱們想在 ObjC 代碼中調用 Swift 的代碼,只須要寫上一行 import "ProductModuleName-Swift.h"(這裏的 ProductModuleName表示 target 的名字)就能夠了,由於在編譯時,編譯器會自動給項目中的 Swift 代碼生成一個 ProductModuleName-Swift.h 的頭文件(這個文件是編譯產物,咱們在 build 目錄能夠看到它),暴露給 ObjC 使用。shell

圖 3 在 ObjC 中調用 Swift 的代碼

2. 在一個 Framework Target 中混編

除了在一個 App Target 內部混編以外,還有一種狀況是當咱們要寫一個 Library 或者 Framework 給別人用時,這個時候若是有 ObjC 和 Swift 的混編,Objective-C bridging header 的方式已經不適用了,若是咱們用了這個頭文件,Xcode 在預編譯時也會警告咱們。express

先來看看 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>swift

參考

2、踩坑之旅

1. 項目背景

--------------------------------------------------     
              Hotel | HotelOrder | ...                   業務層
--------------------------------------------------
      HotelFoundation | HotelModel | ...               業務基礎層
--------------------------------------------------
  Network | Foundation | MapKit | RNKit | ...          基礎框架層
--------------------------------------------------
 
複製代碼

圖 4 筆者所在公司 iOS 客戶端架構示意圖xcode

目前筆者所在公司的項目總體架構是採用模塊化設計的,並且整個項目徹底都是使用 ObjC/C 實現的,在實際開發時,各模塊既能夠以源碼的形式使用,也能夠以.a + .h + 資源 bundle 的形式使用,簡而言之,既能夠源碼依賴,也能夠是靜態庫依賴。那麼咱們能夠直接在項目中使用 Swift 靜態庫嗎?

圖 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 代碼中添加 @objcpublic 等關鍵字
    • 在 ObjC 代碼中添加 #import <SwiftLibA/SwiftLibA-Swift.h>(這裏的 SwiftLibA 是新添加的靜態庫的名字)

圖 6 在主工程中集成 Swift 靜態庫工程

圖 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 一下,發如今連接時仍然會報錯。

圖 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

圖 9 Swift 標準庫

參考:

4. 動態連接問題

靜態連接的問題已經解決了,此時按下 ⌘+R,模擬器啓動後發生崩潰。控制檯上的日誌信息顯示 dyld: Library not loaded: @rpath/libswiftCore.dylib,這是由於程序啓動時 Swift 動態庫加載失敗了。

圖 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

圖 12 添加 Runpath Search Path

Always Embed Swift Standard Library 曾經叫作 Embedded Content Contains Swift Code,字面上看上去像是「老是嵌入 Swift 標準庫」,可是實際上這裏只是告訴 build system 要作的事,並不表明用戶手機上下載的 app 是這樣的,由於在發佈 app 時,app thinning 會自動根據目標系統來決定是否將 app bundle 中的 Swift 標準庫刪掉。

圖 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

圖 14 設置 Always Embed Swift Standard Library

參考:

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

圖 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

複製代碼

圖 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'

圖 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 文件則保存了源代碼中的註釋內容。

圖 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 stabilityUpdate on Module Stability and Module Interface Files

圖 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 文件。

圖 20 Build Libraries for Distribution 選項

圖 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 中。

圖 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

參考

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 the std 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 the std module allows the compiler to build the std 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. The module.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 out header 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,這樣就大功告成了。

圖 23 新建 umbrella header 文件

圖 24 新建 modulemap 文件

圖 25 添加 modulemap 文件的路徑到 SwiftLibA 的 import paths

圖 26 添加 umbrella header 文件的路徑到 SwiftLibA 的 header search paths

若是你的 Swift 模塊要調用的模塊是 ObjC-Swift 混編的,也可用一樣的方式來實現,核心點就在於將 C-based 語言的頭文件用 modulemap 和 umbrella header 封裝起來。

參考:

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.

複製代碼

圖 27 調試 Swift 代碼時沒法看到變量值

這是由於主工程中沒有 Swift 代碼,因此就沒有 Swift 相關的環境和設置選項,解決辦法就是在主工程中建立一個新的 Swift 文件。

3、總結

Swift 5 的到來終於讓咱們看到了期待已久的 ABI 穩定,相信更現代、更安全的 Swift 會變得愈來愈流行。另外,在模塊化/組件化項目中落地 Swift 時,LLVM Module 是一個繞不過去的話題,LLVM Module 改變了傳統 C-Based 語言的頭文件機制,取而代之的是 Module 的思惟。技術的發展會帶來更先進的生產力,咱們期待 Swift 在將來可以進一步提高咱們的開發效率和編程體驗。

相關文章
相關標籤/搜索