<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 Swift 混編及組件化落地

          共 11533字,需瀏覽 24分鐘

           ·

          2021-03-31 10:50

          背景


          自 Swift 誕生以來,逐步見證其從飽受詬病到日漸完善。在蘋果的全力推動(dòng)下,潛移默化地把開發(fā)支持中心從 Objective-C 轉(zhuǎn)向 Swift,在業(yè)界的呼聲也越演越烈。當(dāng)我們相繼迎來 ABI穩(wěn)定、Module stability、Library evolution 等功能后,我們期盼已久的 Swift 已然到來,毅然啟動(dòng)了京東 App 的混編之旅。我們依然堅(jiān)持穩(wěn)扎穩(wěn)打,前期對(duì) Swift 技術(shù)做了諸多調(diào)研工作,具體可見Swift環(huán)境及編譯優(yōu)化調(diào)研2020年7月京東 App 的首個(gè)混編版本上線蘋果商店,完成了組件內(nèi)和主工程的混編工作;近期,我們完成了對(duì)京東組件化管理工具(iBiuTool)的改造,混編組件化功能正式落地,這也標(biāo)志著京東 Swift 混編基礎(chǔ)支持建設(shè)完畢。但是,Just the beginning...


          期待的Swift已經(jīng)到來


          2.1
          ABI穩(wěn)定



          Swift 5.0,提供 ABI 穩(wěn)定,解決了 Swift runtime 的版本兼容問題。這意味著通過 Swift 5.0 及以上的編譯器編譯出來的二進(jìn)制,就可以運(yùn)行在任意 Swift 5.0 及以上的 Swift runtime 上。ABI 穩(wěn)定后,Swift runtime 和標(biāo)準(zhǔn)庫已經(jīng)植入 macOS 10.14.4、iOS 12.2、watchOS 5.2 及以上系統(tǒng)中。根據(jù)蘋果官方數(shù)據(jù),截止到 2020年12月15日,四年內(nèi)發(fā)布的 iPhone 設(shè)備中 iOS 13及以上占比已達(dá) 98%。

          另外,ABI 穩(wěn)定還帶來了性能上的提升。由于 Swift runtime 已經(jīng)被深入的集成在了設(shè)備的操作系統(tǒng)中,并且結(jié)合系統(tǒng)層做了許多優(yōu)化,這就使得 Swift 程序具有更快的啟動(dòng)速度、更好的運(yùn)行性能,以及更少的內(nèi)存占用量。


          2.2
          Module Stability



          Swift 5.1,支持 Module Stability,解決模塊間編譯器版本兼容的問題。這意味著使用不同版本編譯器構(gòu)建的 Swift 模塊可以在同一個(gè)應(yīng)用程序中一起使用。即使某些三方庫的 Swift 編譯器版本與你所使用的不同,也不會(huì)存在編譯問題。官方文檔中舉了一個(gè)十分恰當(dāng)?shù)睦樱褂?Swift 6 構(gòu)建的 framwork,可以被 Swift 6 和未來的 Swift 7 編譯器正常使用。所以這個(gè)進(jìn)化對(duì)于開發(fā)者來說,絕對(duì)是一件非常美好事情。

          在 Swift 中有一個(gè) .swiftmodule 文件,它是一種二進(jìn)制文件,主要包含模塊中的數(shù)據(jù)信息和內(nèi)部編譯器的數(shù)據(jù)結(jié)構(gòu)。由于內(nèi)部編譯器的數(shù)據(jù)結(jié)構(gòu)的存在,同一個(gè)模塊編譯的 swiftmodule 文件在不同版本的編譯器中都是不一樣的。這也就是為什么在某個(gè)版本編譯器中編譯的二進(jìn)制文件,在另一個(gè)版本編譯器中無法被導(dǎo)入使用的原因。



          Module Stability 解決了這個(gè)問題,在模塊穩(wěn)定后,存儲(chǔ)模塊信息的文件已經(jīng)替代為 swiftinterface 格式了。它是一個(gè)文本格式的文件,它包含所有 public 或者 open 的 API 以及一些隱式的代碼或者 API,還包括 swiftinterface 的版本、生成此 swiftinterface 的編譯器版本,以及 Swift 編譯器將其作為模塊導(dǎo)入時(shí)所需的命令行標(biāo)志的子集。而且這些 API 與源代碼很類似,通過源碼穩(wěn)定實(shí)現(xiàn)了模塊穩(wěn)定。


          2.3
          Library Evolution



          Swift 5.1, 支持 Library Evolution,解決了二進(jìn)制庫向下兼容的問題。在 Library Evolution 特性開啟的狀態(tài)下,二進(jìn)制庫某些場(chǎng)景下的 API 更新后,就會(huì)自動(dòng)實(shí)現(xiàn)對(duì)舊版本庫的兼容。Library Evolution 可以在不破壞二進(jìn)制兼容性的情況下對(duì)庫進(jìn)行某些修改。

          舉例來具體說明一下這個(gè)問題。組件 B 和組件 C 都依賴了組件 A,他們的組件版本都是 v1.0。主工程的 v1.0 發(fā)布時(shí),這三個(gè)組件需要各種構(gòu)建,并集成到主工程中。如下圖所示:

            

            

          當(dāng)主工程 v2.0 發(fā)布時(shí),組件 A 對(duì)組件 B 在 v1.0 版本所使用的 API 進(jìn)行了一些 resilient 的修改,但這些修改并沒有影響到組件 C。所以,組件 B 在構(gòu)建二進(jìn)制庫時(shí),就需要更新依賴的組件 A 到 v2.0 版本。而組件 C 沒有功能修改,則不需要更新依賴和發(fā)布新版本。然后,他們都集成到 v2.0 版本的主工程中。


              

          如果組件 A 的 Library Evolution 在沒有啟用的情況下,在組件 C 中與組件 A 相關(guān)的代碼就有可能在運(yùn)行時(shí)產(chǎn)生問題、甚至崩潰。而開啟 Library Evolution 后,就能夠做到對(duì)舊版本的兼容。

              

          混編的方式


          京東 App 根本上是一個(gè)基于 Cocoapods 實(shí)現(xiàn)的組件化工程,總的來看需要?jiǎng)澐譃閮蓚€(gè)場(chǎng)景:一、主工程的混編;二、各組件內(nèi)的混編。在這兩個(gè)場(chǎng)景中,對(duì) Swift 引入 ObjC 和 ObjC 引入 Swift又做了不同的處理。


          3.1
          工程中 - Swift 調(diào)用 ObjC



          在主工程的 Target 下,需要通過橋接頭文件的方式,將 ObjC 的頭文件暴露給 Swift 進(jìn)行使用。

              

          • 創(chuàng)建橋接文件(-Bridging-Header.h)
          • 確保 Build Setting 中 SWIFT_OBJC_BRIDGING_HEADER 為該橋接文件的路徑
          • 將需要引入到 Swift 的 ObjC 的頭文件添加進(jìn)去


          3.2
          工程中 - ObjC 調(diào)用 Swift



          在主工程的 Target 下,可以通過引入 Swift Module 的 ObjC Interface Header的方式,在 ObjC 中使用 Swift。由于 Swift Module的緣故,所以引入一個(gè)文件,便可以使用該模塊下的所有 Swift 文件。需要注意的是,這個(gè)頭文件的命名默認(rèn)是"ProjectName-Swift.h",如果工程名中有一些 nonalphanumeric 字符,則會(huì)被替換為下劃線。

          • 確保 Build Setting 中 SWIFT_OBJC_INTERFACE_HEADER_NAME 的配置正確
          • 在 ObjC 中引入該模塊的 Swift 頭文件,#import "XXX-Swift.h"
          • 若在 ObjC的 .h 中引入,則可以通過向前聲明的方式,@class XXX


          3.3
          組件內(nèi) - Swift 調(diào)用 ObjC



          在同一個(gè) .framework 或者 .a 中實(shí)現(xiàn) Swift 調(diào)用 ObjC,通過 Bridging-Header 的方式是無法解決的。如果你嘗試使用 Bridging-Header 的方式,并且通過 .podspec 對(duì) Bridging-Header 進(jìn)行配置寫入。只會(huì)有短暫性的編譯成功,最終將會(huì)報(bào)錯(cuò):



          經(jīng)過官方文檔中的近一步查證,發(fā)現(xiàn)在同一個(gè) framework 中的 Swift 想要引入 ObjC,需要將該 ObjC 文件導(dǎo)入到其 umbrella-header 文件中。這樣 Swift 模塊就可以對(duì) umbrella-header 中向外暴露的類進(jìn)行調(diào)用了。另外,官方文檔中還提到 DEFINES_MODULE 要配置為 YES,這樣整個(gè)組件就可以作為一個(gè)模塊被外部導(dǎo)入使用了。


          3.4
          組件內(nèi) - ObjC 調(diào)用 Swift



          在同一個(gè) .framework 或者 .a 中實(shí)現(xiàn) ObjC 調(diào)用 Swift,依然需要通過引入 Swift Module 的 ObjC Interface Header。

          • 確保 Build Setting 中 SWIFT_OBJC_INTERFACE_HEADER_NAME 的配置正確
          • 在 ObjC 中引入該模塊的 Swift 頭文件,.framework 中為 #import <XXX/XXX-Swift.h>,.a 中為 #import "XXX-Swift.h"。
          • 若在 ObjC的 .h 中引入,則可以通過向前聲明的方式,@class XXX


          組件內(nèi)混編


          4.1
          組件內(nèi)混編實(shí)施方案



          參照上文中梳理的大體方案,便可以對(duì)京東App組件進(jìn)行混編實(shí)施。京東組件通過自己的工具進(jìn)行組件管理的,由于歷史原因,某些方面的功能還不能完全支持,比如 modulemap、module stability、new build setting。所以這一些問題需要繞過,這些問題文章后面會(huì)針對(duì)說明。具體的實(shí)施步驟如下:


          • 組件內(nèi)添加 Swift 文件,且不需要?jiǎng)?chuàng)建橋接文件
          • podspec 中 source_files 配置中添加 swift 項(xiàng)
          • ObjC 調(diào)用 Swift

          o 在蘋果官方文檔中,推薦配置 DEFINES_MODULE = YES,并通過 #import <XXX/XXX-Swift.h> 的方式導(dǎo)入 Swift Module。但在京東組件中動(dòng)態(tài)庫是以 .framework 形式存在的,需要以 #import <XXX/XXX-Swift.h> 的方式導(dǎo)入 Swift Module;而靜態(tài)庫是以 .a 形式存在的,需要以 #import "XXX-Swift.h" 的方式導(dǎo)入。

          o 值得注意的是,要確保你的 Swift 類為 Public 或 Open 的訪問權(quán)限,否則在 Swift Module 之外的 ObjC 文件中是無論如何都不能調(diào)用的。對(duì)于 ObjC 中想要使用屬性和函數(shù),需要標(biāo)記 @objc,它會(huì)告訴編譯器該屬性或者函數(shù)能夠應(yīng)用于 Objective-C 代碼中。而且標(biāo)有 @objc 特性的類必須繼承自 ObjC 的類。

          • Swift 調(diào)用 ObjC

          o Swift 模塊想要調(diào)用 ObjC 就需要將 ObjC 的頭文件暴露在 umbrella-header 中。這樣就需要在 podspec 中將 public_header_files 配置中添加要暴露的 ObjC 頭文件后,供 Swift 進(jìn)行調(diào)用。


          4.2
          組件內(nèi)混編通信方案



          按照上述方案實(shí)施后,組件內(nèi)的通信歸為 ObjC 調(diào)用 Swift 和 Swift 調(diào)用 ObjC 兩個(gè)方面。具體通信方式如下圖所示:



          組件間混編


          5.1
          Swift 調(diào)用 ObjC



          Swift 調(diào)用 ObjC API 前,首先需要通過 import module 語法找到對(duì)應(yīng)的模。Module機(jī)制是在2013年加入了Xcode中,目的是為了提升編譯速度,解決C、C++中#include機(jī)制的一些遺留問題。具體是如何解決的,可以看下這兩篇文章:關(guān)于objective-cmodules和autolinking(1)、Clang官方文檔(2)。

          我們需要解決的問題是如何讓編譯器找到Module,通過查看 Clang 官方的文檔,我們發(fā)現(xiàn):

          • 如果要支持Module,必須提供一個(gè)module.modulemap文件,用來聲明模塊與頭文件之間的映射關(guān)系
          • 針對(duì) framework,Clang 會(huì)通過指定路徑查找命名為module.modulemap的文件:.framework/Modules/module.modulemap


          module.modulemap文件中的內(nèi)容大致如下,主要是用來聲明模塊與頭文件之間的映射關(guān)系,支持 import module 方式調(diào)用。


          framework module STStaticBasicStableModule {       umbrella header "STStaticBasicStableModule-umbrella.h"       export *       module * {  export * }   }   module STStaticBasicStableModule.Swift {       header "STStaticBasicStableModule-Swift.h"       requires objc   }

           

          找到模塊,并且知道模塊有哪些頭文件后,就可以訪問組件提供的類、方法了。


          5.2
          Swift 調(diào)用 Swift



          我們知道 ObjC 代碼之間調(diào)用 API 是通過頭文件的形式,但 Swift 是沒有頭文件的,它使用一個(gè)二進(jìn)制格式的文件(.swiftmodule)來代替頭文件,這個(gè)文件中包含了 Swift 模塊的所有 API、inlinable function bodies。

          編譯器會(huì)去哪找 swiftmodule 文件呢?我們?cè)?swift 源碼中找到了一些蛛絲馬跡:

           // SerializedModuleLoader.cpp   void SerializedModuleLoaderBase::collectVisibleTopLevelModuleNamesImpl(       SmallVec torImpl<Identifier> &names, StringRef extension) const {     // ...     forEachModuleSearchPath(Ctx, [&](StringRef searchPath, SearchPathKind Kind,                                      bool isSystem) {       switch (Kind) {       // ...       case SearchPathKind::Framework: {         // 源碼中的注釋及相關(guān)代碼說明了 swiftmodule 在 framework 中的查找機(jī)制         // Look for:         // $PATH/{name}.framework/Modules/{name}.swiftmodule/{arch}.{extension}         forEachDirectoryEntryPath(searchPath, [&](StringRef path) {           // ...         });         return None;       }       }       llvm_unreachable("covered switch");     });   }


          .swiftmodule是一個(gè)序列化后的二進(jìn)制文件,從文件名SerializedModuleLoader.cpp可以猜測(cè)這個(gè)是負(fù)責(zé)加載.swiftmodule文件的。另外上述代碼中也說明了針對(duì) framework,Swift 編譯器如何查找.swiftmodule。

          同時(shí)為了保證組件支持 x86、arm 架構(gòu)下編譯,我們還需要將不同架構(gòu)編譯生成的 swiftmodule 文件手動(dòng)合并到最終的 framework 中。


          5.3
          ObjC 調(diào)用 Swift



          編譯器會(huì)通過我們編寫的 Swift 代碼生成xxx-Swift.h,這樣 ObjC 就可以通過這個(gè)頭文件訪問 Swift 的 API 了。我們可以通過兩種 import 方式調(diào)用 Swift API:

          • @import STStaticBasicStableModule
          • #import "STStaticBasicStableModule-Swift.h"


          還記得上面 modulemap 中的STStaticBasicStableModule.Swift吧,@import STStaticBasicStableModule在找到模塊后,會(huì)通過 modulemap 文件中聲明找到STStaticBasicStableModule-Swift.h:

           module STStaticBasicStableModule.Swift {       header "STStaticBasicStableModule-Swift.h"       requires objc   } 


          xxx-Swift.h也需要支持多架構(gòu),我們需要把不同架構(gòu)下生成的xxx-Swift.h內(nèi)容合并到一個(gè)文件中,最終合并的xxx-Swift.h,去掉部分代碼,大致結(jié)構(gòu)是這個(gè)樣子:

           #ifndef TARGET_OS_SIMULATOR   #include <TargetConditionals.h>   #endif       #if TARGET_OS_SIMULATOR           // Release-iphonesimulator/Swift Compatibility Header/XXX-Swift.h       #if 0       #elif defined(__x86_64__) && __x86_64__       // __x86_64__       #elif defined(__i386__) && __i386__       // __i386__       #endif       #else           // Release-iphoneos/Swift Compatibility Header/XXX-Swift.h       #if 0       #elif defined(__arm64__) && __arm64__       // __arm64__       #elif defined(__ARM_ARCH_7A__) && __ARM_ARCH_7A__       // __ARM_ARCH_7A__       #endif       #endif // TARGET_OS_SIMULATOR 


          5.4
          Module stability & Library evolution



          文章開篇說過,它們是 Swift 5.1 新增的2個(gè)關(guān)于二進(jìn)制穩(wěn)定的特性,可以支持發(fā)布和共享 framework。只有當(dāng)你的庫要獨(dú)立于客戶端進(jìn)行構(gòu)建的情況下,才需要開啟 Build Libraries for Distribution 選項(xiàng),而且 Module Stability 和 Library Evolution 就會(huì)同時(shí)生效。通常我們?cè)陂_發(fā)二進(jìn)制庫時(shí),最好盡早打開此開關(guān),以提供任何二進(jìn)制兼容性的保證。

          如果不支持這2個(gè)特性,可能會(huì)出現(xiàn)的問題:

          • User1 使用 Xcode 11.2 發(fā)布了基礎(chǔ)組件,User2 依賴了這個(gè)組件,并使用 Xcode 11.7 編譯,結(jié)果:編譯報(bào)錯(cuò)Module compiled with Swift 5.1.2 cannot be imported by the Swift 5.2.4compiler
          • User1 給結(jié)構(gòu)體中新增了一個(gè)屬性,然后發(fā)布了新版本組件,依賴該組件的下游較多,User1 需要周知所有依賴方依賴最新版本重新編譯,否則可能會(huì)引發(fā)運(yùn)行時(shí)崩潰

          開啟這2個(gè)特性的方式:

          • Xcode 中設(shè)置 build setting 中的 BUILD_LIBRARY_FOR_DISTRIBUTION 為 YES
          • 需要支持 New Build System。


          5.5
          組件間混編通信方案



          按照上述方案實(shí)施后,組件間的通信歸為 ObjC 調(diào)用 Swift 和 Swift 調(diào)用 ObjC,以及 Swift 調(diào)用 Swift 三個(gè)方面。具體通信方式如下圖所示:



          京東主工程混編支持方案


          6.1
          靜態(tài)編譯的問題



          由于我們之前是純 ObjC 的開發(fā)環(huán)境,所以即使實(shí)現(xiàn)了組件內(nèi)混編,京東組件化的主工程(或者組件的Example工程)也并不能成功編譯。原因在于它們還不支持 Swift 混編環(huán)境,在編譯時(shí)可能會(huì)報(bào)類似錯(cuò)誤:



          或者



          上述的錯(cuò)誤信息說明,編譯器不能自動(dòng)鏈接到 Swift 相關(guān)的一些靜態(tài)庫和動(dòng)態(tài)庫。而這些資源是存在于 Xcode Toolchains 下的,在本地的路徑為:

          • /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos
          • /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/iphoneos

          接下來將這些資源路徑配置在工程中,一般地需要在Build Settings -> Library Search Paths 中添加:

          • "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
          • "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"


          6.2
          動(dòng)態(tài)庫加載的問題



          當(dāng)配置好可鏈接資源的路徑之后,就可以成功編譯了。但在啟動(dòng)時(shí),動(dòng)態(tài)庫加載的問題將會(huì)引起程序的崩潰。諸如以下錯(cuò)誤:



          如果你的設(shè)備是 iOS 12.2 及以上,可能報(bào)錯(cuò)如下:



          在 Build Settings -> Runpath Search Paths 中首行添加配置 /usr/lib/swift 配置,特別要注意的是只能在首行配置才能解決問題。

          如果你的設(shè)備是 iOS 12.2 以下,可能報(bào)錯(cuò)如下:



          iOS 12.2 以下,將 Build Settings -> Always Embed Swift Standard Libraries 設(shè)置為 YES。

          這兩個(gè)問題都是在應(yīng)用啟動(dòng)后,動(dòng)態(tài)庫加載時(shí),發(fā)生的崩潰。那為什么要以 iOS 12.2 為分水嶺呢?這就是從 iOS 12.2 Swift 已經(jīng)實(shí)現(xiàn) ABI 穩(wěn)定了。所以上述兩處的解決方案缺一不可,因?yàn)樗鼈兎謩e針對(duì) ABI 穩(wěn)定前后的系統(tǒng)版本。這些配置完成后,京東組件的主工程(或者組件的Example工程)就已經(jīng)完全支持 Swift 混編環(huán)境了。另外,還有一種更加便捷方案也可以達(dá)到同樣的效果。


          6.3
          一鍵配置混編環(huán)境



          除了 Xcode Build Settings 配置的方式,還可以通過在工程中新建一個(gè) Swift 文件(文件中無需添加任何代碼),通過這種方式 Xcode 會(huì)自動(dòng)完成部分環(huán)境配置,能夠解決靜態(tài)編譯問題和部分設(shè)備的動(dòng)態(tài)庫加載問題。另外,還需要處理 iOS 12.2 以下 Swift 動(dòng)態(tài)庫加載的問題。將 Always Embed Swift Standard Libraries 設(shè)置為 YES。

          假如通過 Xcode Build Settings 配置的方式,從 Xcode 11 beta 4 開始,就需要在工程配置中添加 swift-5.0 的新配置項(xiàng)了。但如果通過新建 Swift 文件的方式,就不必更新配置,它的優(yōu)勢(shì)在于機(jī)動(dòng)性好。

          京東是一個(gè)標(biāo)準(zhǔn)的組件化應(yīng)用,主工程中無代碼實(shí)現(xiàn),所以不需要實(shí)現(xiàn)混編代碼,僅需要使其支持 Swift 開發(fā)環(huán)境即可。因此 Xcode Build Settings 配置的方式能夠滿足需求,無需多余文件,在工程的簡(jiǎn)潔性上更好。


          通信方案總結(jié)


          最后,對(duì)京東 App 中涵蓋的混編通信方式做個(gè)匯總。以組件內(nèi)、組件間,以及主工程的混編形式為基礎(chǔ),將整體的混編通信方式匯總?cè)缦拢?/p>




             

          作者:王彥昌、姚琦、林曉峰


          參考文獻(xiàn)

          *(1)https://onevcat.com/2013/06/new-in-xcode5-and-objc/#%E5%85%B3%E4%BA%8Eobjective-cmodules%E5%92%8Cautolinking

          *(2)https://clang.llvm.org/docs/Modules.html

          *https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift

          *https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_swift_into_objective-c

          * http://clang.llvm.org/docs/Modules.html

          * https://swift.org/blog/library-evolution/

          * https://swift.org/blog/abi-stability-and-more/

          *https://forums.swift.org/t/plan-for-module-stability/14551

          *https://forums.swift.org/t/learning-about-swifts-lack-of-header-files/13215/5

          * https://github.com/apple/swift

          *https://github.com/apple/swift-evolution/blob/master/proposals/0260-library-evolution.md

          瀏覽 66
          點(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>
                  中文无码人妻 | 黄色视频大全在线观看 | 黄色免费视频永久免费 | 国产精品久久久豆花视频 | 天天舔天天爱天天干 |