1. 百度App Objective-C/Swift 組件化混編之路 - 實踐篇

        共 14900字,需瀏覽 30分鐘

         ·

        2021-05-13 22:03

        概述

        前文《百度App Objective-C/Swift 組件化混編之路(二)- 工程化》已經(jīng)介紹了百度App 組件內(nèi) Objective-C/Swift 混編、單測、以及組件間依賴、二進制發(fā)布、集成的工程化過程。下面重點介紹百度App 組件化 Objective-C/Swift 組件化混編改造實踐,希望能對大家有所啟發(fā)和幫助。

        組件化混編改造

        百度App 經(jīng)過組件化和二進制化改造后,組件的編譯產(chǎn)物主要是 static_framework   (.framework) 和 static_library(.a)兩種類型,因此百度App 混編主要是圍繞 static_framework 和 static_library 進行。

        Swift 5.0 ABI(Application Binary Interface)穩(wěn)定后,操作系統(tǒng)統(tǒng)一了 ABI 標(biāo)準(zhǔn),編譯出的二進制產(chǎn)物能在不同的 runtime 下運行。但 ABI 穩(wěn)定是使用二進制發(fā)布框架(binary frameworks)的必要非充分條件。隨后的 Swift 5.1 又推出了 Module Stability 特性,使不同版本的 Swift 編譯器生成的二進制可以在同一個應(yīng)用程序中使用,這才掃除二進制廣泛高效使用的障礙。

        丨1 static_library 的問題

        在 Xcode 中, static_framework 的 Build Settings 設(shè)置 BUILD_LIBRARY_FOR_DISTRIBUTION(見圖1)為 YES,就開啟了 Module Stability。

        圖1

        而在 static_library 中, BUILD_LIBRARY_FOR_DISTRIBUTION 設(shè)置為 YES 后編譯組件,會產(chǎn)生using bridging headers with module interfaces is unsupported 錯誤。可見 bridging header 與 Swift 的二進制接口文件(.swiftinterface)無法兼容,無法做到 Module Stability。

        由于在 static_library 內(nèi)混編會導(dǎo)致不同 Swift 編譯器版本上生成的二進制不兼容,所以 static_library 要支持組件內(nèi)混編和二進制兼容性發(fā)布必須做 Framework 化(static_framework)改造,下面詳細說明改造過程。

        2 組件 Framework 化改造

        • 將 static_library 改成 static_framework,百度App 借助 EasyBox 可以快速完成,例如:

        # 在 Boxfile 文件內(nèi),將 target 從 :static_library 修改為 :static_framework# 然后 box install 就完成轉(zhuǎn)化box 'BBAUserSetting', '2.2.6', :target => :static_framework
        • 修改相關(guān)頭文件引用方式,例如:

        // static_library 引用頭文件方式 #import "ComponentA.h"
        // static_framework 引用頭文件方式 #import <ComponentA/ComponentA.h>

        去預(yù)編譯頭文件 和 規(guī)范公開頭文件

        百度App 部分 static_library 含有預(yù)編譯頭文件(pre-compiled header),主要作用是加快編譯速度。將公開頭文件放入預(yù)編譯頭文件中,組件內(nèi)的頭文件和源文件不用再逐一顯式引用,但 pch 的使用有兩個問題:

        • 不能作為二進制形態(tài)組件的一部分。

        • 當(dāng) static_library 改造成 static_framework(支持 module 可以提升編譯速度)后,會導(dǎo)致組件內(nèi)頭文件缺少公用頭文件的引用,造成組件編譯錯誤。

        基于以上原因,需要刪除預(yù)編譯頭文件,規(guī)范組件公開的頭文件,明確組件內(nèi)頭文件的引用,盡量減少公開頭文件和接口的數(shù)量。

        組件 Module 化

        LLVM Module 改變了傳統(tǒng) C-Based 語言的頭文件機制,被 Swift 采用,如果組件沒有 Module 化,Swift 就無法調(diào)用該組件,如何 Module 化 見 《百度App Objective-C/Swift 組件化混編之路(二)- 工程化》。

        組件 module 化編譯產(chǎn)物的目錄結(jié)構(gòu)如下:
        ├── 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 要求有明確的依賴,并且會傳遞依賴,組件公開頭文件依賴不明確,就有可能導(dǎo)致編譯錯誤,例如:組件 A 依賴組件 B,組件 B 依賴組件 C,且組件 B 的對外暴露頭文件引用了組件 C,那么組件 B 依賴傳遞了組件 C,組件 A 也必須依賴組件 C 或者組件 B 聲明傳遞依賴組件 C,否則在 module 化(配置 module.modulemap)的情況下會出現(xiàn) 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)內(nèi)混編

        在 static_framework 中, Swift 通過 module 中的文件訪問 Objective-C 定義的公開數(shù)據(jù)類型和接口,Objective-C 通過 #import<ProductName/ProductModuleName-Swift.h> 訪問 Swift 定義的公開數(shù)據(jù)類型和接口。

        目前百度App 的 static_framework 默認會將所有 public header 公開出來,然后在 umbrella header 文件內(nèi)引用了這些 public header,這樣 Swift 文件就可以直接調(diào)用到。美中不足的是如果 Objective-C 頭文件是 static_framework 私有頭文件,為了 Objective-C/Swift 混編且能夠被 Swift 文件調(diào)用到,需要將這些私有頭文件改成公開頭文件,詳情見 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 文件調(diào)用 Swift,需要在 Swift 類前面要用 open 或 public 修飾,以及滿足其他互操作性要求。

        8 組件混編理想態(tài)

        組件內(nèi)混編只是中間態(tài),理想態(tài)是單個組件完全使用 Swift;而組件間混編,是一個長期存在的形態(tài),最終某個組件要么是 Swift 組件,要么是 Objective-C 組件,調(diào)用方式比較簡單,static_framework 內(nèi)的 Swift 文件使用直接 import 其他組件,例如:

        // static_framework 內(nèi)的 Swift 文件使用直接 importimport ComponentA

        互操作性

        1 Objective-C APIs Are Available in Swift

        在 Objective-C 的頭文件里,點擊左上角的 Related Items 按鈕,選擇 Generated Interface 后,就可以查看 Objective-C API 自動生成對應(yīng)的 Swift API,如圖所示:

        2 Nullability for Objective-C

        // 在 Objective-C 頭文件中沒有加上// NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END
        @interface ObjClass : NSObject// objClassString 值有可能為空@property (nonatomiccopyNSString *objClassString; // getObjClassInstance 值有可能為空- (ObjClass *)getObjClassInstance; @end
        // Objective-C 轉(zhuǎn)化 Swift 代碼后open class ObjClass : NSObject {    open var objClassString: String!    open func getInstance() -> ObjClass!}
        // 在 Swift 文件中調(diào)用
        let cls = ObjClass.init()print(cls.getInstance().objClassString)

        在 Objective-C 頭文件中沒有加上 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END,轉(zhuǎn)化 Swift 代碼后,對應(yīng)的返回值會轉(zhuǎn)換為隱式解析可選類型(implicitly unwrapped optionals),如果直接使用 getObjClassInstance,返回值為空就會導(dǎo)致 crash。

        // 在 Objective-C 頭文件中加上// NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END
        NS_ASSUME_NONNULL_BEGIN@interface ObjClass : NSObject// objClassString 值有可能為空@property (nonatomiccopyNSString *objClassString; // getObjClassInstance 值有可能為空- (ObjClass *)getObjClassInstance; @endNS_ASSUME_NONNULL_END
        // Objective-C 轉(zhuǎn)化 Swift 后open class ObjClass : NSObject { open var objClassString: String open func getInstance() -> ObjClass }
        // 在 Swift 文件中調(diào)用let cls = ObjClass.init() print(cls.getInstance().objClassString)

        在 Objective-C 頭文件中加上 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 標(biāo)明屬性或者方法返回值不能為空,實際上業(yè)務(wù)方不注意還是有可能返回空,在這種情況下轉(zhuǎn)化為對應(yīng)的 Swift 代碼,不會轉(zhuǎn)換為隱式解析可選類型(implicitly unwrapped optionals),直接使用不會 crash ,所以建議在 Objective-C 頭文件中開始和結(jié)束分別加上 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END。

        3 安全集合類型參照實現(xiàn)

        // Objective-C NSArray 沒有指定類型// Objective-C@interface UIView@property(nonatomic,readonly,copyNSArray *subviews;@end// Swiftclass UIView { var subviews: [Any] { get } }   
        // Objective-C NSArray 指定類型// Objective-C@interface UIView@property(nonatomic,readonly,copyNSArray<UIView *> *subviews;@end// Swiftclass UIView { var subviews: [UIView] { get }}

        在 Objective-C 中的 NSArray 可以插入不同類型,當(dāng)聲明屬性沒有指定類型 @property (nonatomicstrongreadonlyNSArray *subviews 轉(zhuǎn)化 Swift 后就變成open var subviews: [Any] { get },這時候在 Swift 中使用數(shù)組 subviews 里面的對象,需要通過 as? 進行判斷是否是 UIView 類型,所以在 Objective-C 中聲明數(shù)組的時候,聲明指定類@property (nonatom-icstrongreadonlyNSArray<UIView *> *subviews 轉(zhuǎn)換 Swift 后也是指定類型,獲取數(shù)據(jù)更安全。

        4 Objective-C/Swift 混編關(guān)鍵字

        • @objc  聲明 Swift 類中需要暴露給 Objective-C 的方法要用關(guān)鍵字 @objc

        • @objc(name)  聲明修改 Swift 類中需要暴露給 Objective-C 的方法名稱

        • @nonobjc  聲明 Swift 類中不暴露給 Objective-C 的方法要用關(guān)鍵字 @nonobjc

        • @objcMembers  聲明 Swift 類會隱式地為所有的屬性或方法添加 @objc 標(biāo)識,聲明為 @objc 的類需要繼承自 NSObject ,而 @objcMembers 不需要繼承自 NSObject,但是這種情況下 Objective-C 就不能訪問 Swift 類的方法或者屬性

        • dynamic  聲明 dynamic 使得 Swift 具有動態(tài)派發(fā)特性

        Objective-C 是動態(tài)語言,所有方法、屬性都是動態(tài)派發(fā)和動態(tài)綁定的,而 Swift 卻相反,它一共包含三種方法分派方式:Static dispatch,Table dispatch Message dispatch。在 Swift 類中聲明為 @objc 的屬性或方法有可能會被優(yōu)化為靜態(tài)調(diào)用,不一定會動態(tài)派發(fā),如果要使用動態(tài)特性,需要將 Swift 類的屬性或方法聲明為 @objc dynamic,此時 Swift 的動態(tài)特性將使用 Objective-C Runtime 特性實現(xiàn),完全兼容 Objective-C。

        @objc dynamic func testViewController() {}
        • NS_SWIFT_UNAVAILABLE  在 Swift 中不可見,不能使用

        + (instancetype)collectionWithValues:(NSArray *)values                             forKeys:(NSArray<NSCopying> *)keysNS_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 調(diào)用 Objective-C 的 API 時可能由于數(shù)據(jù)類型等不 一致導(dǎo)致無法達到預(yù)期(例如,Objective-C 里的方法采用了 C 語言風(fēng)格的多參數(shù)類型;或者 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) }}

        常見問題

        1. 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)

        2. static_framework 內(nèi) Swift 文件調(diào)用 Objective-C 文件,如果該 Objective-C 公開頭文件內(nèi)引用其他組件的公開頭文件,且這個組件沒有 module 化(配置 module.modulemap)就會出現(xiàn)  include of non-modular header inside framework module  錯誤,因此公開給 Swift 調(diào)用的組件都是需要 module 化,例如:

          // 在 ComponentA 組件的 ComponentA.h 頭文件內(nèi),引用 ComponentB 組件的公開頭文件// ComponentB 組件剛好沒有 module 化(配置module.modulemap)#import <ComponentB/ComponentB.h>
        3. static_framework 組件內(nèi)的 Swift 文件調(diào)用 static_library 組件,需要將 static_library module 化(配置 module.modulemap),否則不能在 Swift 文件內(nèi)直接使用,在 Xcode debug Swift 文件時,發(fā)現(xiàn) Swift 文件內(nèi)調(diào)用的 static_library,如果 static_library 的頭文件寫法有問題,在 Xcode 控制臺打印 self 例如 "po self",就會出現(xiàn)  Error while loading Swift module  錯誤,例如:

          // 在 static_library 的 ComponentA.h 頭文件內(nèi)// 錯誤的寫法#import "TestA.h"#import "TestB.h"
          // 正確的寫法// 需要修改暴露的頭文件,不然會導(dǎo)致無法加載 Swift module#if __has_include(<ComponentA/ComponentA.h>)#import <ComponentA/TestA.h>#import <ComponentA/TestB.h>#else#import "TestA.h"#import "TestB.h"#endif
        4.  Cycle Reference Error  

          在說明 Cycle Reference 之前先看一下錯誤信息 

          error: Cycle inside XXX; building could produce unreliable results.

           

        下面通過舉例具體分析一下
        • 在 static_library 中,如前面所述,Objective-C/Swift 混編是通過 bridging header 作為橋接,假設(shè) 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 中 假設(shè) ComponentA 里面有個 Swift 類 MySwiftClass,Objective-C 的 MyObjcClass 頭文件中使用了該 Swift 類,需要引用 ComponentA-Swift.h 頭文件,例如:

        #import "ComponentA/ComponentA-Swift.h" // 測試過程中通過這種方式引用會導(dǎo)致 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 進行混編時,編譯過程大致如下:
        • 預(yù)編譯處理 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 還沒編譯完成,就有可能導(dǎo)致編譯錯誤。

        建議:在 Objective-C/Swift混編中,盡量保持單向引用(OC 類引用 Swift 類或者 Swift 類引用 OC 類),減少循環(huán)引用,特殊情況可以使用前置聲明(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

        總結(jié)

        隨著 Apple 大力推進、開源社區(qū)對 Swift 支持,Swift 普及已經(jīng)大勢所趨,目前百度App 經(jīng)過 EasyBox 工具鏈支持混編組件二進制打包,以及組件的改造,業(yè)務(wù)層 30% 組件可以使用 Swift 混編開發(fā),不支持混編的業(yè)務(wù)層組件也在陸續(xù)改造中,服務(wù)層(百度App組件化之路)及以下組件(占總組件數(shù)比 55% )都可以使用 Swift 混編開發(fā),并在基礎(chǔ)功能清理緩存、Feed 等相關(guān)業(yè)務(wù)完成 Swift 混編落地。作為 iOS 開發(fā)的你,還在等什么,趕緊升級技術(shù)棧吧!

        參考資料

        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:張渝、王文軍、陳松、陳佳、李政、趙家祝

        瀏覽 46
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 日本黄色一级A片 | 男女操逼免费看 | 国产偷窥熟女高潮精品视频免费啪 | 9久精品视频 | 国产91在线观看丝袜 |