<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          百度App Objective-C/Swift 組件化混編之路 - 實(shí)踐篇

          共 14900字,需瀏覽 30分鐘

           ·

          2021-05-13 22:03

          概述

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

          組件化混編改造

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

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

          丨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 后編譯組件,會(huì)產(chǎn)生using bridging headers with module interfaces is unsupported 錯(cuò)誤??梢?bridging header 與 Swift 的二進(jìn)制接口文件(.swiftinterface)無(wú)法兼容,無(wú)法做到 Module Stability。

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

          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 的使用有兩個(gè)問題:

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

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

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

          組件 Module 化

          LLVM Module 改變了傳統(tǒng) C-Based 語(yǔ)言的頭文件機(jī)制,被 Swift 采用,如果組件沒有 Module 化,Swift 就無(wú)法調(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 要求有明確的依賴,并且會(huì)傳遞依賴,組件公開頭文件依賴不明確,就有可能導(dǎo)致編譯錯(cuò)誤,例如:組件 A 依賴組件 B,組件 B 依賴組件 C,且組件 B 的對(duì)外暴露頭文件引用了組件 C,那么組件 B 依賴傳遞了組件 C,組件 A 也必須依賴組件 C 或者組件 B 聲明傳遞依賴組件 C,否則在 module 化(配置 module.modulemap)的情況下會(huì)出現(xiàn) Could not build module 'XX' 編譯錯(cuò)誤。

          組件間依賴傳遞
          # A.boxspec 的配置,聲明組件 A 依賴組件 B s.dependency 'B' 
          # B.boxspec 的配置,聲明組件 B 依賴組件 C s.dependency 'C'
          ## 解決方案一 # A.boxspec 的配置,增加組件 C 的依賴 s.dependency 'C'
          ## 解決方案二 # Swift 的 module 會(huì)傳遞依賴,百度App 使用 EasyBox 的 module_dependency 來(lái)解決這個(gè)問題 # 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 默認(rèn)會(huì)將所有 public header 公開出來(lái),然后在 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)是單個(gè)組件完全使用 Swift;而組件間混編,是一個(gè)長(zhǎng)期存在的形態(tài),最終某個(gè)組件要么是 Swift 組件,要么是 Objective-C 組件,調(diào)用方式比較簡(jiǎn)單,static_framework 內(nèi)的 Swift 文件使用直接 import 其他組件,例如:

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

          互操作性

          1 Objective-C APIs Are Available in Swift

          在 Objective-C 的頭文件里,點(diǎn)擊左上角的 Related Items 按鈕,選擇 Generated Interface 后,就可以查看 Objective-C API 自動(dòng)生成對(duì)應(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 代碼后,對(duì)應(yīng)的返回值會(huì)轉(zhuǎn)換為隱式解析可選類型(implicitly unwrapped optionals),如果直接使用 getObjClassInstance,返回值為空就會(huì)導(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)明屬性或者方法返回值不能為空,實(shí)際上業(yè)務(wù)方不注意還是有可能返回空,在這種情況下轉(zhuǎn)化為對(duì)應(yīng)的 Swift 代碼,不會(huì)轉(zhuǎn)換為隱式解析可選類型(implicitly unwrapped optionals),直接使用不會(huì) crash ,所以建議在 Objective-C 頭文件中開始和結(jié)束分別加上 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END。

          3 安全集合類型參照實(shí)現(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 },這時(shí)候在 Swift 中使用數(shù)組 subviews 里面的對(duì)象,需要通過 as? 進(jìn)行判斷是否是 UIView 類型,所以在 Objective-C 中聲明數(shù)組的時(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 類會(huì)隱式地為所有的屬性或方法添加 @objc 標(biāo)識(shí),聲明為 @objc 的類需要繼承自 NSObject ,而 @objcMembers 不需要繼承自 NSObject,但是這種情況下 Objective-C 就不能訪問 Swift 類的方法或者屬性

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

          Objective-C 是動(dòng)態(tài)語(yǔ)言,所有方法、屬性都是動(dòng)態(tài)派發(fā)和動(dòng)態(tài)綁定的,而 Swift 卻相反,它一共包含三種方法分派方式:Static dispatch,Table dispatch Message dispatch。在 Swift 類中聲明為 @objc 的屬性或方法有可能會(huì)被優(yōu)化為靜態(tài)調(diào)用,不一定會(huì)動(dòng)態(tài)派發(fā),如果要使用動(dòng)態(tài)特性,需要將 Swift 類的屬性或方法聲明為 @objc dynamic,此時(shí) Swift 的動(dòng)態(tài)特性將使用 Objective-C Runtime 特性實(shí)現(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í)可能由于數(shù)據(jù)類型等不 一致導(dǎo)致無(wú)法達(dá)到預(yù)期(例如,Objective-C 里的方法采用了 C 語(yǔ)言風(fēng)格的多參數(shù)類型;或者 Objective-C 方法返回值是 NSNotFound,在 Swift 中期望返回 nil)。這時(shí)候就可以使用 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 一致會(huì)造成 .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)引用其他組件的公開頭文件,且這個(gè)組件沒有 module 化(配置 module.modulemap)就會(huì)出現(xiàn)  include of non-modular header inside framework module  錯(cuò)誤,因此公開給 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 文件時(shí),發(fā)現(xiàn) Swift 文件內(nèi)調(diào)用的 static_library,如果 static_library 的頭文件寫法有問題,在 Xcode 控制臺(tái)打印 self 例如 "po self",就會(huì)出現(xiàn)  Error while loading Swift module  錯(cuò)誤,例如:

            // 在 static_library 的 ComponentA.h 頭文件內(nèi)// 錯(cuò)誤的寫法#import "TestA.h"#import "TestB.h"
            // 正確的寫法// 需要修改暴露的頭文件,不然會(huì)導(dǎo)致無(wú)法加載 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 之前先看一下錯(cuò)誤信息 

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

             

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

          #import "ComponentA/ComponentA-Swift.h" // 測(cè)試過程中通過這種方式引用會(huì)導(dǎo)致 Cycle Reference 問題// #import <ComponentA/ComponentA-Swift.h> // 測(cè)試通過這種方式引用正常
          @interface MyObjcClass : NSObject - (MySwiftClass *)returnSwiftClassInstance; // ... @end

          而 MyObjcClass 又在 MySwiftClass 中使用,需要將 MyObjcClass.h 頭文件加入到 umbrella header 中,例如:

          // ComponentA.h 在百度App 默認(rèn)組件名稱 .h 就是作為 static_framework 的 umbrella header#import <ComponentA/MyObjcClass.h>
          Objective-C 與 Swift 進(jìn)行混編時(shí),編譯過程大致如下:
          • 預(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 頭文件,此時(shí)由于 Swift 還沒編譯完成,就有可能導(dǎo)致編譯錯(cuò)誤。

          建議:在 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 大力推進(jìn)、開源社區(qū)對(duì) Swift 支持,Swift 普及已經(jīng)大勢(shì)所趨,目前百度App 經(jīng)過 EasyBox 工具鏈支持混編組件二進(jìn)制打包,以及組件的改造,業(yè)務(wù)層 30% 組件可以使用 Swift 混編開發(fā),不支持混編的業(yè)務(wù)層組件也在陸續(xù)改造中,服務(wù)層(百度App組件化之路)及以下組件(占總組件數(shù)比 55% )都可以使用 Swift 混編開發(fā),并在基礎(chǔ)功能清理緩存、Feed 等相關(guān)業(yè)務(wù)完成 Swift 混編落地。作為 iOS 開發(fā)的你,還在等什么,趕緊升級(jí)技術(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
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免免费国产AAAAA片牛牛影视 | 日本黄色电影网站视频 | 豆花视频免费版 | 天天色天天插 | 97操逼网|