Swift framework 與 OC 混編

前言

我們團隊是做即時通信類 SDK 的開發,提供了聊天、會話、群組、關系鏈等基礎的 IM 功能。

SDK 采用跨平臺開發技術,所有業務邏輯均采用 C++ 開發,各個平臺均只提供接口封裝,以保持邏輯一致性,目前 SDK 提供了 Objective-C、Java、C++ 以及跨平臺 C 接口。

眾所周知,Apple 大力推行 Swift 開發語言,目前海外市場大多數同類型廠商均提供了純 Swift 的接口。由于國內市場的慣性,針對 iOS 平臺,我們提供的是 Objective-C 的接口。

于是,整個 Swift 的適配就有了 2 大類解決方案:

  • Objective-C 使用 Nonnull、Nullable、NS_SWIFT_NAME 來適配 Swift 的語法,借助 XCode 及橋接文件自動適配 Swift;
  • 在現有 SDK 的基礎上提供一套完整的純 Swift 接口的 framework。

細心的讀者一眼就可以看出,第一種解決方案等同于什么都不做,而是將 Swift 和 Objective-C 工程混編交給了客戶,讓其在業務層處理無法預見的工程混編問題。這種方案顯得不專業,不符合 SDK 簡單接入的初衷。XCode 將 Objective-C 導入到 Swift 后,會自動生成 Swift 語法,從而引起接口名稱、枚舉值等的混亂,影響了各平臺 API 一致性。

由于我們的業務邏輯均是通過 C++ 開發,那么是不是可以意味著直接在上層使用 Swift 封裝即可?

結論是不可以。目前 Swift 僅支持 C 語言和 Objective-C,目前 對 C++ 的支持還處于開發階段,還未正式對外發布,可 查閱官網查看詳情。于是就有了下面 2 大類解決方案:

  • 將目前的 C++ 業務代碼使用 C 做一層跨平臺的接口封裝,提供給 Swift 使用;
  • 使用 Objective-C 對 C++ 業務代碼做一層接口封裝,提供給 Swift 使用。

由于目前 SDK 本身就支持 C 和 Objective-C 接口,所以上面兩種方案本身均是可行的。但因為跨平臺 C 語言跟 Swift 之間混編涉及到數據結構的轉換,以及缺少一些列的基礎組件支持,相當于完全從零開始,工作量相比 Objective-C 來看會大很多。

綜上原因,我們的 Swift 版本 SDK 基于現有 Objective-C 進行封裝,下面將圍繞這一主題展開。

demo 地址:https://github.com/smallyou/swift-oc-interpolation

1 Framework 混編方案

基于 Objective-C 接口封裝,實際上有兩個方案:

  • 方案一:快速封裝,定義 Swift 類繼承自 Objective-C 類,直接暴露給業務層使用即可;
  • 方案二:完全封裝,Swift 層根據 Objective-C 的類結構完全重新定義,把 Objective-C 的細節屏蔽。

但需要注意的是,本次 SDK Swift 化,有兩個需要重點解決的問題:

  • 由于我們需要提供一套全新的 Swift 接口的 SDK(framework),于是就需要我們將 Objective-C 的細節隱藏;
  • 各個平臺 API 要一致,也就是說 Swfit 和 Objective-C 的類名、API 名需要相同,于是就需要我們解決命名沖突的問題。

考慮到上面兩個問題,方案一直接繼承 Objective-C 的類,會將 Objective-C 的細節暴露;即便是不暴露,Swift 化后的 API 頭文件跳轉時看不到類的屬性定義。最終我們選擇方案二。

實際上針對方案二,需要解決下面三個問題:

  • Swift 在 framework 中引入 Objective-C 混編;
  • Swift 的類名和 Objective-C 類名重名;
  • SDK 隱藏 Objective-C 細節。

1.1 模塊化引入 OC 混編

查閱 Apple 的官網,可以看到 Apple 提供了 Swift 和 OC 的 兩種混編方案,針對其中的 framework 混編,蘋果官方建議的方案是在 Swift 中使用 umbrella 文件后作如下操作:

  • 在當前 framework target 的 Build Settings 中,找到 Packaging,確保 Defines Module 設置為 Yes;
  • 然后在 Swfit 的 umbrella 頭文件中引入 OC 的類。

接著你就可以在 Swift 的 framework target 中的任意 swift 代碼下使用 OC 的類了。

官方建議的方案,有兩個非常大的問題:

  • 由于 Swift 和 OC 的類名一致,此時在 Swift 中會出現類名沖突的問題;

  • 由于 Swift 的 umbrella header 文件會暴露給外部客戶,也就意味著外部客戶也可以使用 OC 的類。

所以上述方案實際上不可行,至少在我們本次 SDK Swift 化中不可行。

那么如何引入 OC,并且還可以解決重名的問題呢?

我們知道 C++ 和 Java 都各自有 namespace 和 packagename 來解決命名沖突的問題,那么 Swfit 和 OC 中是不是也有類似的呢?

我們知道,在 Swift 中使用系統庫 Foundation、UIKit 時,只需要 import Foundation 或者 import UIKit 即可。實際上,Swift 結合 umbrella header 將對應的代碼模塊化了,只要 import 模塊名即可。

也就是說,我們是不是也可以將 OC 的代碼模塊化,提供一個 OC 的 module 給 Swift 使用,通過 modulename.classname 的語法格式來避免類名沖突。那么如何將 OC 代碼模塊化呢?

LLVM 很早就提供了 module 的技術,并使用 modulemap 文件來描述頭文件和 module 之間的關系。

我們本次也是基于 modulemap 來將 OC 代碼模塊化,可以通過下面兩個步驟來實現:

  1. 新建 modulemap 文件,描述 OC 的頭文件所在的具體位置;
  2. 在 Swift 的 framework target 的 Build Settings 中,找到 Swift Compiler,將剛才的 modulemap 所在的路徑填充到 Import Paths 選項中。

1.2 隱藏 OC 細節

按照上述的 modular OC 代碼后引入 Swift 后,我們可以直接使用 import 語句使用 OC 的代碼。

// import oc module
import IMEngine

public class IMManager {
    // import oc class
    private let engine =  IMEngine.IMManager.share()
}

如果想要隱藏 OC 的細節(頭文件、協議等),就需要在 Build Phases 的 Header 中,將所有 OC 的頭文件移動到 Project 中。

此時構建出來的 framework 看不懂任何 OC 的細節,但同時也引入了一個新問題。

當你將構建出來的 framework 導入到其他設備的工程中(注意,是其他設備,非本機打包設備)后開始編譯,就會出現這個錯誤:"Missing required module 'IMEngine' "。

出現這個錯誤的原因是,我們將 OC 的信息隱藏了,但是又在 Swift 中 import 了 IMEngine。Swift 官網論壇上也有人提出了 類似的問題 ,針對該問題,目前已經新增了一個 @_implementationOnly 屬性

根據描述,我們可以將其粗略理解成僅聲明模塊,在實際實現中才開始導入模塊。類似我們在 OC 頭文件中使用 @class A @protocol B 來聲明類和協議,不影響編譯過程。

需要注意的是,Swift 模塊中使用 @_implementationOnly import IMEngine 之后,Swift 中所有的 public 類型的 API 均不能繼承 IMEngine 的類,也不能實現 IMEngine 中的協議。如果有類似的需求,可以新增一個 Swift 內部對象來中轉。

????例如,Swift 的類 IMManager 想實現 OC 中的 SDKListner,由于 IMManager 是 public 類型,故不能直接實現 SDKListener 協議。可以在內部定義一個 internal 類型的 Swift class IMListenerProxy,讓其作為 IMManager 的屬性來實現 SDKListener。

// import oc module
@_implementationOnly import IMEngine

public class IMManager {
    
    // import oc class
    private let engine =  IMEngine.IMManager.share()
    
    // proxy of IMSDKListener
    private lazy var listenerProxy: IMSDKListenerProxy = {
        [weak self] in
        let proxy = IMSDKListenerProxy()
        proxy.delegate = self
        return proxy
    }()
    
    public static let shared: IMManager = IMManager()
}


// Access control: internal
// functions: Implementaion of IMEngine.SDKListener
class IMSDKListenerProxy : NSObject, IMEngine.IMSDKListener {
    weak var delegate: IMManager?
    
    func onLogined() {
        if let delegate = delegate {
            // ...
        }
    }
}

2 具體實現

講解具體的配置步驟之前,我們先看下 SDK 的整體目錄結構。

|--------- imsdk

|-------- | --------- imsdk.xcodeproj

|-------- | --------- imsdk

|-------- | --------- |-------- objective-c

|-------- | --------- |-------- |-------- header

|-------- | --------- |-------- |-------- |-------- IMManager.h

|-------- | --------- |-------- |-------- src

|-------- | --------- |-------- |-------- |-------- IMManager.m

|-------- | --------- |-------- swift

|-------- | --------- |-------- |-------- header

|-------- | --------- |-------- |-------- |-------- IMManager.swift

|-------- | --------- |-------- |-------- src

|-------- | --------- |-------- |-------- |-------- IMInnerr.swift

|-------- | --------- |-------- |-------- modulemap (無需加入 imsdk.xcodeproj 管理)

|-------- | --------- |-------- |-------- |-------- module.modulemap(無需加入 imsdk.xcodeproj 管理)

2.1 使用 modulemap 導入 OC 的模塊

在任意位置新建一個 modulemap 文件夾,并在其中新增一個 module.modulemap 文件。可以參考上面的工程示意圖。

注意:

  • 該文件的名稱一定是 module.modulemap,否則 XCode 無法 import 進來。
  • 該文件的位置與 OC 源代碼/頭文件的位置不做要求,僅僅需要注意的是 modulemap 文件內部引用頭文件一定是 OC 頭文件的相對位置。

modulemap 文件內容如下,具體的語法格式,可以參考 modulemap

module IMEngine [extern_c] {
        // 此處的頭文件是相對于當前 modulemap 文件的位置
    umbrella header "../../objective-c/header/IMManager.h"
    export *
}

經過上述操作后,OC 的代碼目前已經模塊化了,在 Swift 的 framework target 中設置 Build Settings,Packaging 的 Defines Module 為 Yes。


1.png

接下來,在 Swift 的 framework target 中設置 Build Settings, Swift Compiler 的 Import Paths 屬性,將其設置為 module.modulemap 的文件路徑。


2.png

做完上述兩個操作后,你就可以在 swift 中使用 OC 的類混編了。

2.2 使用 @_implementationOnly 導入 OC 類隱藏細節

隱藏 OC 的細節,最關鍵的是不要直接使用 import IMEngine 來導入 OC 的 IMEngine。需要使用 @_implementationOnly import IMEngine 方式導入。

如下實例代碼,在開發過程中,需要嚴格注意,所有 public 類型的 Swift API 均不能直接繼承、實現 OC 的類和協議。

//---------------模塊導入---------------
// 錯誤導入,不能直接導入 IMEngine,否則外部客戶編譯是會提示 "Missing required module 'IMEnging'"
import IMEngine
// 正確導入
@_implementationOnly import IMEngine
//------------------------------------

//--------------類封裝--------------
// 錯誤使用,將 OC 的IMEngine 信息暴露給了 public 類型的 API
public class IMManager : IMEngine.IMManager {
  
}
// 正確使用
public class IMManager {
    // 使用內部屬性來包裝 OC 的類,可以是 internal 也可以是 private
    private let engine =  IMEngine.IMManager.share()
}
//----------------------------------


//-------------協議實現-----------------
// 錯誤使用, public 類型的類不能直接實現 OC 的協議,否則會引起報錯
public class IMManager : NSObject, IMEngine.IMSDKListener {
        
}


// 正確使用
public class IMManager {
    // 內部類,作為 IMManager 的屬性,做協議實現后的中轉
    private lazy var listenerProxy: IMSDKListenerProxy = {
        [weak self] in
        let proxy = IMSDKListenerProxy()
        proxy.delegate = self
        return proxy
    }()
}
// 使用內部類 IMSDkListenerProxy 來實現 OC 的協議
class IMSDKListenerProxy : NSObject, IMEngine.IMSDKListener {
    weak var delegate: IMManager?
    func onLogined() {
        if let delegate = delegate {
            // ...
        }
    }
}
//----------------------------------

2.3 將 Swift 的 framework 導出

首先,找到你 Swift SDK 的 umbrella header 文件。如果沒有,就自行創建一個與 target 同名的頭文件。本示例 demo 中創建了一個名為 imsdk.h 的頭文件。并且將其在 Build Phases 的 Header 中設置為 Public。


3.png

接下里在工程任意位置新建一個與 target 同名的 modulemap 文件。例如,本示例 demo 的 target 名稱為 imsdk,于是新建一個 imsdk.modulemap 文件。

注意:

  • 新建的 modulemap 文件存放的路徑任意
  • modulemap 文件的名稱一定要與 target 的產物名稱一致

modulemap 文件內容如下,具體的語法格式,可以參考 modulemap

// imsdk.modulemap
framework module imsdk {
    // 此處引用你的 umbrella 文件,無需配置路徑,直接名稱即可。
    // 需要注意的是,imsdk.h 需要在 Build Phases 的 Header 中設置為 Public
  umbrella header "imsdk.h"

  export *
  module * { export * }
}

并在 Build Settings 的 Packaging 中設置 Module Map File 為當前 modulemap 的路徑。


4.png

2.4 Framework 腳本打包

Swift 的腳本和 OC 的打包腳本基本一樣,唯一的區別在于合并真機和模擬器架構:

  • OC 只需要調用 lipo create 直接合并即可
  • Swift 除了使用 lipo create 直接合并外,還需要合并 swiftmodule 文件。
    # 編譯模擬器
    ${BUILD_BIN} ${BITCODE_BUILD_OPTION} -project ${IM_SDK_DIR}/imsdk.xcodeproj -target ${BUILD_TARGET} -configuration Release CUSTOM_PRODUCT_NAME=${PRODUCT_NAME} -arch x86_64 -sdk ${BUILD_SDK_IPHONESIMULATOR}
    
    # 編譯真機
    ${BUILD_BIN} ${BITCODE_BUILD_OPTION} -project ${IM_SDK_DIR}/imsdk.xcodeproj -target ${BUILD_TARGET} -configuration Release CUSTOM_PRODUCT_NAME=${PRODUCT_NAME} -sdk ${BUILD_SDK_IPHONEOS}
    if [[ $? -ne 0 ]]; then
        echo "build iphoneos "${BUILD_TARGET}" failed !!! "
        exit 1
    else
        echo "build iphoneos "${BUILD_TARGET}" success !!!"
    fi
    
    # 生成 framework 合并真機和模塊器版本
    cp -rf Release-iphoneos/${PRODUCT_NAME}.framework ${OUTPUT_DIR}/
    
    # 如果是 swift 的話,還需要將 swiftmodule 合并
    if [[ "${SWIFT}" = "Swift" ]] ; then
        cp -r "Release-iphonesimulator/${PRODUCT_NAME}.framework/Modules/${PRODUCT_NAME}.swiftmodule/" "${OUTPUT_DIR}/${PRODUCT_NAME}.framework/Modules/${PRODUCT_NAME}.swiftmodule"
    fi
    
    # 合并二進制
    lipo -create "Release-iphoneos/${PRODUCT_NAME}.framework/${PRODUCT_NAME}" "Release-iphonesimulator/${PRODUCT_NAME}.framework/${PRODUCT_NAME}" -output ${OUTPUT_DIR}/${PRODUCT_NAME}.framework/${PRODUCT_NAME}

3 常見問題

1. 編譯時提示 ”Missing required module 'XXX'“,提示找不到 OC 的 module。

遇到這個問題,多半是 OC 頭文件設置成了 Project,但是在 Swift 封裝過程中 OC 的類又被 Public 類型 Swift API 暴露了。請仔細閱讀上文,使用 @_implementationOnly import,并且嚴格控制 OC 的訪問權限。

2. 用 5.1 的 Swift 編譯器打出來的包無法在其他編譯器上執行?

需要將 Build Settings 的 Build Libraries for Distribution 選項設置為 Yes。

3. 使用腳本在打包 XCFramework 時,打出來的產物是空的

需要將 Build Settings 的 Build Libraries for Distribution 選項設置為 Yes。

4. 打出來的產物,在真機上可以,在模擬器上提示 undefined module。

由于 Swift framework 的 API 信息都存儲在 Modules 里面,在使用 lipo create 合并架構時,還有將 swiftmodule 合并。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容