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

關鍵詞:模塊化/組件化、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 的代碼了。

image

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

image

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

[圖片上傳失敗...(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。

image

圖 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

參考:

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 stabilityUpdate 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

參考

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,這樣就大功告成了。

[圖片上傳失敗...(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 封裝起來。

參考:

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 在未來能夠進一步提升我們的開發效率和編程體驗。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容