前言
我們團隊是做即時通信類 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 代碼模塊化,可以通過下面兩個步驟來實現:
- 新建 modulemap 文件,描述 OC 的頭文件所在的具體位置;
- 在 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。
接下來,在 Swift 的 framework target 中設置 Build Settings, Swift Compiler 的 Import Paths 屬性,將其設置為 module.modulemap 的文件路徑。
做完上述兩個操作后,你就可以在 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。
接下里在工程任意位置新建一個與 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 的路徑。
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 合并。