Python實戰社羣
Java實戰社羣
長按識別下方二維碼,按需求添加
掃碼關注添加客服
進Python社羣▲
掃碼關注添加客服
進Java社羣▲
作者丨吳隆旺
來源丨百度App技術
概述
——
前文《百度App Objective-C/Swift 組件化混編之路(二)- 工程化》已經介紹了百度App 組件內 Objective-C/Swift 混編、單測、以及組件間依賴、二進制發佈、集成的工程化過程。下面重點介紹百度App 組件化 Objective-C/Swift 組件化混編改造實踐,希望能對大家有所啓發和幫助。
組件化混編改造
——
百度App 經過組件化和二進制化改造後,組件的編譯產物主要是 static_framework (.framework) 和 static_library(.a)兩種類型,因此百度App 混編主要是圍繞 static_framework 和 static_library 進行。
Swift 5.0 ABI(Application Binary Interface)穩定後,操作系統統一了 ABI 標準,編譯出的二進制產物能在不同的 runtime 下運行。但 ABI 穩定是使用二進制發佈框架(binary frameworks)的必要非充分條件。隨後的 Swift 5.1 又推出了 Module Stability 特性,使不同版本的 Swift 編譯器生成的二進制可以在同一個應用程序中使用,這才掃除二進制廣泛高效使用的障礙。
丨1 static_library 的問題
在 Xcode 中, static_framework 的 Build Settings 設置 BUILD_LIBRARY_FOR_DISTRIBUTION(見圖1)爲 YES,就開啓了 Module Stability。
圖1而在 static_library 中, BUILD_LIBRARY_FOR_DISTRIBUTION 設置爲 YES 後編譯組件,會產生using bridging headers with module interfaces is unsupported
錯誤。可見 bridging header 與 Swift 的二進制接口文件(.swiftinterface)無法兼容,無法做到 Module Stability。
由於在 static_library 內混編會導致不同 Swift 編譯器版本上生成的二進制不兼容,所以 static_library 要支持組件內混編和二進制兼容性發布必須做 Framework 化(static_framework)改造,下面詳細說明改造過程。
丨2 組件 Framework 化改造
將 static_library 改成 static_framework,百度App 藉助 EasyBox 可以快速完成,例如:
# 在 Boxfile 文件內,將 target 從 :static_library 修改爲 :static_framework
# 然後 box install 就完成轉化
box 'BBAUserSetting', '2.2.6', :target => :static_framework
修改相關頭文件引用方式,例如:
// static_library 引用頭文件方式
#import "ComponentA.h"
// static_framework 引用頭文件方式
#import <ComponentA/ComponentA.h>
丨3 去預編譯頭文件 和 規範公開頭文件
百度App 部分 static_library 含有預編譯頭文件(pre-compiled header),主要作用是加快編譯速度。將公開頭文件放入預編譯頭文件中,組件內的頭文件和源文件不用再逐一顯式引用,但 pch 的使用有兩個問題:
不能作爲二進制形態組件的一部分。
當 static_library 改造成 static_framework(支持 module 可以提升編譯速度)後,會導致組件內頭文件缺少公用頭文件的引用,造成組件編譯錯誤。
基於以上原因,需要刪除預編譯頭文件,規範組件公開的頭文件,明確組件內頭文件的引用,儘量減少公開頭文件和接口的數量。
丨4 組件 Module 化
LLVM Module 改變了傳統 C-Based 語言的頭文件機制,被 Swift 採用,如果組件沒有 Module 化,Swift 就無法調用該組件,如何 Module 化 見 《百度App Objective-C/Swift 組件化混編之路(二)- 工程化》。
組件 module 化編譯產物的目錄結構如下:
├── xxx
├── Headers
│ ├── xxxSettingProtocol.h
│ ├── xxx-Swift.h
├── Info.plist
├── Modules
│ ├── xxx.swiftmodule
│ │ ├── Project
│ │ │ ├── x86_64-apple-ios-simulator.swiftsourceinfo
│ │ │ └── x86_64.swiftsourceinfo
│ │ ├── x86_64-apple-ios-simulator.swiftdoc
│ │ ├── x86_64-apple-ios-simulator.swiftinterface
│ │ ├── x86_64-apple-ios-simulator.swiftmodule
│ │ ├── x86_64.swiftdoc
│ │ ├── x86_64.swiftinterface
│ │ └── x86_64.swiftmodule
│ └── module.modulemap # module 化的情況下
└── _CodeSignature
├── CodeDirectory
├── CodeRequirements
├── CodeRequirements-1
├── CodeResources
└── CodeSignature
5 directories, 18 files
丨5 解決組件間依賴傳遞
Swift Module 要求有明確的依賴,並且會傳遞依賴,組件公開頭文件依賴不明確,就有可能導致編譯錯誤,例如:組件 A 依賴組件 B,組件 B 依賴組件 C,且組件 B 的對外暴露頭文件引用了組件 C,那麼組件 B 依賴傳遞了組件 C,組件 A 也必須依賴組件 C 或者組件 B 聲明傳遞依賴組件 C,否則在 module 化(配置 module.modulemap)的情況下會出現 Could not build module 'XX'
編譯錯誤。
組件間依賴傳遞
# A.boxspec 的配置,聲明組件 A 依賴組件 B
s.dependency 'B'
# B.boxspec 的配置,聲明組件 B 依賴組件 C
s.dependency 'C'
## 解決方案一
# A.boxspec 的配置,增加組件 C 的依賴
s.dependency 'C'
## 解決方案二
# Swift 的 module 會傳遞依賴,百度App 使用 EasyBox 的 module_dependency 來解決這個問題
# B.boxspec 的配置,將直接依賴(dependency)修改成 module 傳遞依賴(module_dependency)組件 C
s.module_dependency 'C'
丨6 開啓 Module Stability
如上面 1 static_library 的問題所述。
丨7 組件(static_framework)內混編
在 static_framework 中, Swift 通過 module 中的文件訪問 Objective-C 定義的公開數據類型和接口,Objective-C 通過 #import<ProductName/ProductModuleName-Swift.h>
訪問 Swift 定義的公開數據類型和接口。
目前百度App 的 static_framework 默認會將所有 public header 公開出來,然後在 umbrella header 文件內引用了這些 public header,這樣 Swift 文件就可以直接調用到。美中不足的是如果 Objective-C 頭文件是 static_framework 私有頭文件,爲了 Objective-C/Swift 混編且能夠被 Swift 文件調用到,需要將這些私有頭文件改成公開頭文件,詳情見 Import Code Within a Framework Target (https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift)。
而 Objective-C 文件調用 Swift,需要在 Swift 類前面要用 open 或 public 修飾,以及滿足其他互操作性要求。
丨8 組件混編理想態
組件內混編只是中間態,理想態是單個組件完全使用 Swift;而組件間混編,是一個長期存在的形態,最終某個組件要麼是 Swift 組件,要麼是 Objective-C 組件,調用方式比較簡單,static_framework 內的 Swift 文件使用直接 import 其他組件,例如:
// static_framework 內的 Swift 文件使用直接 import
import ComponentA
互操作性
——
丨1 Objective-C APIs Are Available in Swift
在 Objective-C 的頭文件裏,點擊左上角的 Related Items 按鈕,選擇 Generated Interface 後,就可以查看 Objective-C API 自動生成對應的 Swift API,如圖所示:
丨2 Nullability for Objective-C
// 在 Objective-C 頭文件中沒有加上
// NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END
@interface ObjClass : NSObject
// objClassString 值有可能爲空
@property (nonatomic, copy) NSString *objClassString;
// getObjClassInstance 值有可能爲空
- (ObjClass *)getObjClassInstance;
@end
// Objective-C 轉化 Swift 代碼後
open class ObjClass : NSObject {
open var objClassString: String!
open func getInstance() -> ObjClass!
}
// 在 Swift 文件中調用
let cls = ObjClass.init()
print(cls.getInstance().objClassString)
在 Objective-C 頭文件中沒有加上 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END,轉化 Swift 代碼後,對應的返回值會轉換爲隱式解析可選類型(implicitly unwrapped optionals),如果直接使用 getObjClassInstance,返回值爲空就會導致 crash。
// 在 Objective-C 頭文件中加上
// NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END
NS_ASSUME_NONNULL_BEGIN
@interface ObjClass : NSObject
// objClassString 值有可能爲空
@property (nonatomic, copy) NSString *objClassString;
// getObjClassInstance 值有可能爲空
- (ObjClass *)getObjClassInstance;
@end
NS_ASSUME_NONNULL_END
// Objective-C 轉化 Swift 後
open class ObjClass : NSObject {
open var objClassString: String
open func getInstance() -> ObjClass
}
// 在 Swift 文件中調用
let cls = ObjClass.init()
print(cls.getInstance().objClassString)
在 Objective-C 頭文件中加上 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 標明屬性或者方法返回值不能爲空,實際上業務方不注意還是有可能返回空,在這種情況下轉化爲對應的 Swift 代碼,不會轉換爲隱式解析可選類型(implicitly unwrapped optionals),直接使用不會 crash ,所以建議在 Objective-C 頭文件中開始和結束分別加上 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END。
丨3 安全集合類型參照實現
// Objective-C NSArray 沒有指定類型
// Objective-C
@interface UIView
@property(nonatomic,readonly,copy) NSArray *subviews;
@end
// Swift
class UIView {
var subviews: [Any] { get }
}
// Objective-C NSArray 指定類型
// Objective-C
@interface UIView
@property(nonatomic,readonly,copy) NSArray<UIView *> *subviews;
@end
// Swift
class UIView {
var subviews: [UIView] { get }
}
在 Objective-C 中的 NSArray 可以插入不同類型,當聲明屬性沒有指定類型 @property (nonatomic, strong, readonly) NSArray *subviews
轉化 Swift 後就變成open var subviews: [Any] { get }
,這時候在 Swift 中使用數組 subviews 裏面的對象,需要通過 as? 進行判斷是否是 UIView 類型,所以在 Objective-C 中聲明數組的時候,聲明指定類@property (nonatom-ic, strong, readonly) NSArray<UIView *> *subviews
轉換 Swift 後也是指定類型,獲取數據更安全。
丨4 Objective-C/Swift 混編關鍵字
@objc 聲明 Swift 類中需要暴露給 Objective-C 的方法要用關鍵字 @objc
@objc(name) 聲明修改 Swift 類中需要暴露給 Objective-C 的方法名稱
@nonobjc 聲明 Swift 類中不暴露給 Objective-C 的方法要用關鍵字 @nonobjc
@objcMembers 聲明 Swift 類會隱式地爲所有的屬性或方法添加 @objc 標識,聲明爲 @objc 的類需要繼承自 NSObject ,而 @objcMembers 不需要繼承自 NSObject,但是這種情況下 Objective-C 就不能訪問 Swift 類的方法或者屬性
dynamic 聲明 dynamic 使得 Swift 具有動態派發特性
Objective-C 是動態語言,所有方法、屬性都是動態派發和動態綁定的,而 Swift 卻相反,它一共包含三種方法分派方式:Static dispatch,Table dispatch 和 Message dispatch。在 Swift 類中聲明爲 @objc 的屬性或方法有可能會被優化爲靜態調用,不一定會動態派發,如果要使用動態特性,需要將 Swift 類的屬性或方法聲明爲 @objc dynamic,此時 Swift 的動態特性將使用 Objective-C Runtime 特性實現,完全兼容 Objective-C。
@objc dynamic func testViewController() {}
NS_SWIFT_UNAVAILABLE 在 Swift 中不可見,不能使用
+ (instancetype)collectionWithValues:(NSArray *)values
forKeys:(NSArray<NSCopying> *)keys
NS_SWIFT_UNAVAILABLE("Use a dictionary literal instead.");
NS_SWIFT_NAME 在 Objective-C中,重新命名在 Swift 中的名稱
// 在 Objective-C 文件中
NS_SWIFT_NAME(Sandwich.Preferences)
@interface SandwichPreferences : NSObject
@property BOOL includesCrust NS_SWIFT_NAME(isCrusty);
@end
@interface Sandwich : NSObject
@end
// 在 Swift 文件中使用
var preferences = Sandwich.Preferences()
preferences.isCrusty = true# 在 Objective-C 文件中
NS_SWIFT_NAME(Sandwich.Preferences)
@interface SandwichPreferences : NSObject
@property BOOL includesCrust NS_SWIFT_NAME(isCrusty);
@end
@interface Sandwich : NSObject
@end
// 在 Swift 文件中使用
var preferences = Sandwich.Preferences()
preferences.isCrusty = true
NS_REFINED_FOR_SWIFT Swift 調用 Objective-C 的 API 時可能由於數據類型等不 一致導致無法達到預期(例如,Objective-C 裏的方法採用了 C 語言風格的多參數類型;或者 Objective-C 方法返回值是 NSNotFound,在 Swift 中期望返回 nil)。這時候就可以使用 NS_REFINED_FOR_SWIFT
// 在 Objective-C 中
@interface Color : NSObject
- (void)getRed:(nullable CGFloat *)red
green:(nullable CGFloat *)green
blue:(nullable CGFloat *)blue
alpha:(nullable CGFloat *)alpha NS_REFINED_FOR_SWIFT;
@end
// 在 Swift 中
extension Color {
var rgba: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 0.0
__getRed(red: &r, green: &g, blue: &b, alpha: &a)
return (red: r, green: g, blue: b, alpha: a)
}
}
常見問題
——
Swift framework module name 和 class name 一致會造成 .swiftinterface file bug (https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962/4)
static_framework 內 Swift 文件調用 Objective-C 文件,如果該 Objective-C 公開頭文件內引用其他組件的公開頭文件,且這個組件沒有 module 化(配置 module.modulemap)就會出現
include of non-modular header inside framework module
錯誤,因此公開給 Swift 調用的組件都是需要 module 化,例如:// 在 ComponentA 組件的 ComponentA.h 頭文件內,引用 ComponentB 組件的公開頭文件 // ComponentB 組件剛好沒有 module 化(配置module.modulemap) #import <ComponentB/ComponentB.h>
在 static_framework 組件內的 Swift 文件調用 static_library 組件,需要將 static_library module 化(配置 module.modulemap),否則不能在 Swift 文件內直接使用,在 Xcode debug Swift 文件時,發現 Swift 文件內調用的 static_library,如果 static_library 的頭文件寫法有問題,在 Xcode 控制檯打印 self 例如 "po self",就會出現
Error while loading Swift module
錯誤,例如:// 在 static_library 的 ComponentA.h 頭文件內 // 錯誤的寫法 #import "TestA.h" #import "TestB.h" // 正確的寫法 // 需要修改暴露的頭文件,不然會導致無法加載 Swift module #if __has_include(<ComponentA/ComponentA.h>) #import <ComponentA/TestA.h> #import <ComponentA/TestB.h> #else #import "TestA.h" #import "TestB.h" #endif
Cycle Reference Error
在說明 Cycle Reference 之前先看一下錯誤信息
error: Cycle inside XXX; building could produce unreliable results.
下面通過舉例具體分析一下
在 static_library 中,如前面所述,Objective-C/Swift 混編是通過 bridging header 作爲橋接,假設 ComponentA 裏面有個 Swift 類 MySwiftClass,Objective-C 的 MyObjcClass 頭文件中使用了該 Swift 類,需要
#import "ComponentA-Swift.h"
頭文件
#import "ComponentA-Swift.h"
@interface MyObjcClass : NSObject
- (MySwiftClass *)returnSwiftClassInstance;
// ...
@end
而 MyObjcClass 又在 MySwiftClass 中使用,需要將 MyObjcClass.h 頭文件加入到 ComponentA-Bridging-Header.h中
// ComponentA-Bridging-Header.h
#import "MyObjcClass.h"
在 static_framework 中 假設 ComponentA 裏面有個 Swift 類 MySwiftClass,Objective-C 的 MyObjcClass 頭文件中使用了該 Swift 類,需要引用 ComponentA-Swift.h 頭文件,例如:
#import "ComponentA/ComponentA-Swift.h" // 測試過程中通過這種方式引用會導致 Cycle Reference 問題
// #import <ComponentA/ComponentA-Swift.h> // 測試通過這種方式引用正常
@interface MyObjcClass : NSObject
- (MySwiftClass *)returnSwiftClassInstance;
// ...
@end
而 MyObjcClass 又在 MySwiftClass 中使用,需要將 MyObjcClass.h 頭文件加入到 umbrella header 中,例如:
// ComponentA.h 在百度App 默認組件名稱 .h 就是作爲 static_framework 的 umbrella header
#import <ComponentA/MyObjcClass.h>
Objective-C 與 Swift 進行混編時,編譯過程大致如下:
預編譯處理 bridging header 或者 umbrella header,然後編譯 Swift 源文件,再 merge swiftmodule
Swift 編譯完成後,生成 ProjectName-Swift.h 的頭文件供 Objective-C 使用
最後編譯 Objective-C 源文件
因此,編譯 Swift 需要先處理 bridging header 或者 umbrella header,而 bridging header 或者 umbrella header 裏面的 MyObjcClass.h 又引用 ComponentA-Swift.h 頭文件,此時由於 Swift 還沒編譯完成,就有可能導致編譯錯誤。
建議:在 Objective-C/Swift混編中,儘量保持單向引用(OC 類引用 Swift 類或者 Swift 類引用 OC 類),減少循環引用,特殊情況可以使用前置聲明(Forward Declaration),解決 Circle Reference,參考 Include Swift Classes in Objective-C Headers Using Forward Declarations (https://developer.apple.com/documentation/swift/import-ed_c_and_objective-c_apis/importing_objective-c_into_swift)
@class MySwiftClass;
@interface MyObjcClass : NSObject
- (MySwiftClass *)returnSwiftClassInstance;
// ...
@end
總結
——
隨着 Apple 大力推進、開源社區對 Swift 支持,Swift 普及已經大勢所趨,目前百度App 經過 EasyBox 工具鏈支持混編組件二進制打包,以及組件的改造,業務層 30% 組件可以使用 Swift 混編開發,不支持混編的業務層組件也在陸續改造中,服務層(百度App組件化之路)及以下組件(佔總組件數比 55% )都可以使用 Swift 混編開發,並在基礎功能清理緩存、Feed 等相關業務完成 Swift 混編落地。作爲 iOS 開發的你,還在等什麼,趕緊升級技術棧吧!
參考資料
——
1. Importing Objective-C into Swift (https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift)
2. Importing Swift into Objective-C (https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_swift_into_objective-c)
3. Swift and Objective-C Interoperability (https://developer.apple.com/videos/play/wwd-c2015/401/)
4. Nullability and Objective-C (https://developer.apple.com/swift/blog/?id=25)
5. Library Evolution in Swift (https://swift.org/blog/library-evolution/)
6. Improving Objective-C API Declarations for Swift (https://developer.apple.com/documentation/swift/objective-c_and_c_code_customization/improving_objective-c_api_declarations_for_swift)
7. Making Objective-C APIs Unavailable in Swift (https://developer.apple.com/documentation/swift/objective-c_and_c_code_customization/making_objective-c_apis_unavailable_in_swift)
8. Renaming Objective-C APIs for Swift (https://developer.apple.com/documentation/sw-ift/objective-c_and_c_code_customization/renaming_objective-c_apis_for_swift)
Reviewer:張渝、王文軍、陳松、陳佳、李政、趙家祝
程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣
近期精彩內容推薦:
在看點這裏好文分享給更多人↓↓