免费人成动漫在线播放r18-免费人成观看在线网-免费人成黄页在线观看日本-免费人成激情视频在线观看冫-jlzzjlzz亚洲大全-jlzzjlzz亚洲日本

二維碼
企資網

掃一掃關注

當前位置: 首頁 » 企資頭條 » 頭條 » 正文

Swift_與_Objective_C_混編

放大字體  縮小字體 發布日期:2021-09-05 02:29:59    作者:企資小編    瀏覽次數:52
導讀

作者 | 趙志、曾慶隆、顧夢奇、王強、趙發出品 | CSDN(ID:CSDNnews)2019 年 3 月 25 日,蘋果發布了 Swift 5.0 版本,宣布了 ABI 穩定,并且Swift runtime 和標準庫已經植入系統中,而且蘋果新出文檔都用 Swift,

作者 | 趙志、曾慶隆、顧夢奇、王強、趙發

出品 | CSDN(ID:CSDNnews)

2019 年 3 月 25 日,蘋果發布了 Swift 5.0 版本,宣布了 ABI 穩定,并且Swift runtime 和標準庫已經植入系統中,而且蘋果新出文檔都用 Swift,Sample Code 也是 Swift,可以看出 Swift 是蘋果扶持與研發的重點方向。

目前國內外各大公司都在相繼試水,只要關注 Swift 在國內 iOS 生態圈現狀,你就會發現,Swift 在國內 App 應用的比重逐漸升高。對于新 App 來說,可以直接用純 Swift 進行開發,而對于老 App 來說,絕大部分以前都是用 OC 開發的,因此 Swift/OC 混編是一個必然面臨的問題。

CSDN 付費下載自視覺中國

Swift 和 OC 混編開發

關于 Swift 和 OC 間如何混編,業內也已經有很多相關文章詳細講解,簡單來說 OC/Swift 調用 Swift,最終通過 Swift Module 進行,而 Swift 調用 OC 時,則是通過 Clang Module,當然也可以通過 Clang Module 進行 OC 對 OC 的調用。58同城于 2020 年正式上線首個 Swift/OC(Objective-C,以下簡稱 OC)項目,與此同時,也在全公司范圍內開展了一個多部門協作項目——混天項目,主要目標:

一是提供混編的基礎設施建設,如提供通過的 Module 化方案;

二是擴展各工具鏈的混編能力,如對無用類檢測工具 WBBlades(github/wuba/WBBlades)進行 Swift 能力的擴展;

三是對已有的基礎庫進行 Module 化和 Swift 適配;

四是將混編開發在各 App 和各業務線中推廣和落地。

我們在 Module 化實踐中發現,實際數據與蘋果官方 Module 編譯時間數據不一致,于是我們通過 Clang 源碼和數據相結合的方式對 Clang Module進行了深入研究,找到了耗時的原因。由于 Swift/OC 混編下需要 Module 化的支持,同時借鑒業內 HeaderMap 方案讓 OC 調用 OC 時避開 Module 化調用,將編譯時間優化了約 35%,較好地解決了在 Module 化下的編譯時間問題。

Clang Module 初探

Clang Module 在 2012 LLVM Developers Meeting 上第一次被提出,主要用來解決 C 語言預處理的各種問題。Modules 試圖通過隔離特定庫的接口并且編譯一次生成高效的序列化文件來避免 C 預處理器重復解析 Header 的問題。在探究 Clang Module 之前,我們先了解一下預處理的前世今生。

一個源代碼文件到經過編譯輸出為目標文件主要分為下面幾個階段:

源文件在經過 Clang 前端包含:詞法分析(Lexical analysis) 、語法分析(Syntactic analysis) 、語義分析(Semantic analysis)。最后輸出與平臺無關的 IR(LLVM IR generator)進而交給后端進行優化生成匯編輸出目標文件。

詞法分析(Lexical analysis)作為前端的第一個步驟負責處理源代碼的文本輸入,具體步驟就是將語言結構拆分為一組單詞和記號(token),跳過注釋,空格等無意義的字符,并將一些保留關鍵字轉義為定義好的類型。詞法分析過程中遇到源代碼 “#“ 的字符,且該字符在源代碼行的起始位置,則認為它是一個預處理指令,會調用預處理器(Preprocessor)處理后續。在開發中引入外部文件的 include/import 指令,定義宏 define 等指令均是在預處理階段交由預處理器進行處理。Clang Module 機制的引入帶來的改變著重于解決常規預處理階段的問題,那么跟隨我們一起來重點探究一下其中的區別和實現原理吧!

2.1 普通 import 的機制

Clang Module 機制引入之前,在日常開發中,如果需要在源代碼中引入外部的一些定義或者聲明,常見的做法就是使用 #import 指令來使用外部的 API。那么這些使用的方式在預處理階段是怎么處理的呢?

針對編譯器遇到 #import<PodName/header.h> 或者 #import ”header.h” 這種導入方式時候,# 開頭在詞法分析階段會觸發預處理(Preprocessor)。而對于 Clang 的預處理器 import 與 include 指令都屬于它的關鍵詞。預處理器在處理 import Directive 時候主要工作為通過導入的 header 名稱去查找文件的磁盤所在路徑,然后進入該文件創建新的詞法分析器對導入的頭文件進行詞法分析。

如下所示:編譯器在遇到 #import 或者 #include 指令時,觸發預處理機制查詢頭文件的路徑,進入頭文件對頭文件的內容進行解析的流程。

以單個文件編譯過程為維度舉例:在針對一個文件編譯輸出目標文件的過程中,可能會引入多個外界的頭文件,而被引入多個外界頭文件也有可能存在引入外界頭文件。這樣的情況就導致雖然只是在編譯單個文件,但是預處理器會對引入的頭文件進行層層展開。這也是很多人稱 #import 與 include 是一種特殊“復制”效果的原因。

那么在這種預處理器的機制在工程中編譯中會存在什么問題呢?蘋果官方在 2012 的 WWDC 視頻上同樣給了我們解答:Header Fragility (健壯性)和 Inherently Non-Scalable (不可擴展性)。

來看下面一段代碼,在 PodBTestObj 類的文件中定義一個 ClassName 字符串的宏,然后在導入 PoBClass1.h 頭文件,在 PoBClass1.h 的頭文件中同樣定義一個結構體名為 ClassName,這里與我們在 PodBTestObj 類中定義的宏同名。預處理的特殊的“復制”機制,在預處理階段會發生下圖所見的結果:

這樣的問題相信在日常開發中并不罕見,而為了解決這種重名的問題,我們常規的手法只能通過增加前綴或者提前約定規則等方式來解決。

視頻中同時指出這種機制在應對大型工程編譯過程中的所帶來的消耗問題。假設有 N 個源文件的工程,那么每個源文件引用 M 個頭文件,由于預處理的這種機制,我們在針對處理每個源文件的編譯過程中會對引入的 M 個頭文件進行展開,經歷一遍遍的詞法分析-語法分析-語義分析的過程。那么你能想象一下針對系統頭文件的引入在預處理階段將會是一個多么龐大的開銷!

那么針對 C 語言預處理器存在的問題,蘋果有哪些方案可以優化這些存在的問題呢?

2.2 PCH (Precompiled Headers)

PCH(Precompile Prefix Header File)文件,也就是預編譯頭文件,其文件里的內容能被項目中的其他所有源文件訪問。日常開發中,通常放一些通用的宏和頭文件,方便編寫代碼,提高效率。

關于 PCH 的概述,蘋果是這樣定義的:

which uses a serialized representation of Clang’s internal data structures, encoded with the LLVM bitstream format.

(使用 Clang 內部數據結構序列化表示,采用的 LLVM 字節流表示)。

它的設計理念當項目中幾乎每個源文件中都包含一組通用的頭文件時,將該組頭文件寫入 PCH 文件中。在編譯項目中的流程中,每個源文件的處理都會首先去加載 PCH 文件的內容,所以一旦 PCH 編譯完成,后續源文件在處理引入的外部文件時候會復用 PCH 編譯后的內容,從而加快編譯速度。PCH 文件中存放我們所需要的外部頭文件的信息(包括不局限于聲明、定義等)。它以特殊二進制形式進行存儲,而在每個源代碼編譯處理外部頭文件信息時候,不需要每次進行頭文件的展開和“復制”重復操作。而只需要“懶加載”預編譯好的 PCH 內容即可。

存儲內容方面它存放著序列化的 AST 文件。AST 文件本身包含 Clang 的抽象語法樹和支持數據結構的序列化表示,它們使用與 LLVM’s bitcode file format. 相同的壓縮位流進行存儲。關于 AST File 文件的存儲結構你可以在官方文檔有詳細的了解。

它作為蘋果一種優化方案被提出,但是實際的工程中源代碼的引用關系是很復雜的,所以找出一組幾乎所有源文件都包含的頭文件基本不可能,同時對于代碼更新維護更是一個挑戰。其次在被包含頭文件改動下,因為 PCH 會被所有源文件引入,會帶來代碼“污染”的問題。同時一旦 PCH 文件發生改動,會導致大面積的源代碼重編造成編譯時間的浪費。

2.3 Modules

上述我們簡單回顧了一些 C 語言預處理的機制以及為解決編譯消耗引入 PCH 的方案,但是在一定程度上 PCH 方案也存在很大的缺陷。因此在 2012 LLVM Developer’s Meeting 首次提出了 Modules 的概念。

那么 Module 到底是什么呢?

Module 簡單來說可以認為它是對一個組件的抽象描述,包含組件的接口和實現。Module 機制推出主要用來解決上述所闡述的預處理問題,想要探究 Clang Module 的實現,首先需要去開啟 Module。那么針對 iOS 工程怎么開啟 Module 呢? 只需要打開編譯選項中:

對!你沒看錯,僅僅需要在 Xcode 的編譯選項中修改配置即可。

而在代碼的使用上幾乎可以不用修改代碼,開啟 Module 之后,通過引用頭文件的方式可以繼續沿用 #import <PodName/Header.h> 方式。當然對于開發者也可以采用新的方式 @import ModuleName.SubModuleName,以及 @import ModuleName這幾種方式。更為詳細的信息和使用方法可以在蘋果的官方文檔中查看。

2.4 蘋果對 Module 的解讀

上文提到過基于 C 語言預處理器提供的 #include 機制提供的訪問外界庫 API 的方式存在的伸縮性和健壯性的問題。Modules 提供了更為健壯,更高效的語義模型來替換之前 textual preprocessor 改進對庫的 API 訪問方式。

蘋果官方文檔中針對 Module 的解讀有以下幾個優勢:

擴展性:每個 Module 只會編譯一次,將 Module 導入 Translantion unit 的時間是恒定的。對于庫 API 的訪問只會解析一次,將 #include 的機制下的由 M x N 編譯問題簡化為 M + N。

健壯性:每個 Module 作為一個獨立的實體,具備一個一致的預處理環境。不需要去添加下劃線,或者前綴等方式解決命名的問題。每個庫不會影響另外一個庫的編譯方式。

我們翻閱了蘋果 WWDC 2013 的 Advances in Objective-C 視頻,視頻中針對編譯時間性能方面進行了 PCH 和 Module 編譯速度的數據分析。蘋果給出的結論是小項目中 Module 比 PCH 能提升 40% 的編譯時間,并且隨著工程規模的不斷增大,如增大到 Xcode 級別,Module 的編譯速度也會比 PCH 稍快。PCH 也是為了加速編譯而存在的,由此也可以間接得出結論,Module的編譯速度要比沒有 PCH 的情況下,是更快的,如在 Mail 下,應該提升 40% 以上。

對 Clang Module 機制建立一定的認知上,我們著手進行了 Clang Module 在 58同城 App 上的 Module 化改造。

58同城初步實踐

3.1 Module 化工程配置

組件 Module 化

在多 pod 的項目中,通過以下幾種方式可以將各 pod 進行 Module 化:

    Podfile 中添加 use_modular_headers! 對所有的 pod 進行 Module 化;

    Podfile 中通過 modular_headers 對每個 pod 單獨進行 Module 化,如對 PodC 進行 Module 化,pod 'PodC', :path => '../PodC',:modular_headers => true;

    在 pod 所對應的 .podspecs 中的 xcconfig 中 sg 配置 DEFINES_MODULE,如 s.xcconfig = {'DEFINES_MODULE' => 'YES'}。

此外,為了能讓其它組件能通過 module 方式引用 Module 化的組件,還需要設置它們之前的依賴關系。

在58同城中,維護了一個全局的依賴配置文件 dependency.json,這個文件通過自動化工具進行維護,各組件 pod 的 .podspecs 從 dependency.json 中動態讀取自己依賴的其它組件,并生成相應的 dependency 關系。

3.2 Swift/OC 混編橋接文件

通常在 Swift/OC 混編工程中會自動或手動在當前pod添加加一個橋接文件,如 PodC-Bridging-Header.h,配置當前 pod 中 Swift 需要引用的 OC 文件,形式如下所示。

這樣可以達到編譯的目的,但是由于依賴的組件都是在橋接文件中統一配置,對于每個 Swift 文件依賴了哪些 pod 組件,實際上并不清楚,而且 Swift 中每次修改新增一個 OC 文件的引用,都需要在橋接文件中進行修改,并且如果是減少對某個 OC 文件的引用,也不好確定是否要在橋接文件中進行刪除,因為還需要判斷其它 Swift 文件中是否有引用。

Swift 文件中可以通過 module 的方式去引用 OC 文件,因此,如果所依賴 OC 文件的 pod 都 Module 化后,可以通過 import module 的方式進行引用,每個 Swift 文件各自維護對外部 pod 的依賴,從而將 XXX-Bridging-Header.h 文件刪除,也減少了對橋接文件的維護成本。

3.3 同城的 Module 化編譯數據

萬事具備,只差編譯!

結合蘋果官方給出了性能數據,我們預測 Module 化后的編譯速度是要比非 Module 情況更快,那不妨就編譯試試,接下來在 58同城中分別在 module 和非 module 場景下進行編譯。

通過編譯數據,我們看到的結果發生了逆轉,Module 化之后的時間竟然比非 Module 情況下長約 8%,這跟剛才我們看到的蘋果官方數據不符,有點亂了。需要說明的是這份數據是 58同城全業務線在 M1 機器上運行出來的,并且把資源復制的環節從配置中刪除了,即不包含資源復制時間,是純代碼編譯時間,并且在非 M1 機器上也運行了進行對比,除了時間長些,結論基本也是 module 化之后時間長 10% 左右。

在面對實際測試結果 Module 化之后的編譯耗時更長的情況下,我們從更深層次上進行對 Clang Module 原理進行了探究。

Clang Module 原理深究

Clang Module 機制的引入主要是為了解決預處理器的各種問題,那么工程在開啟 Module 之后,工程上會有哪些變化呢?同時在編譯過程中編譯器工作流程與之前又有哪些不同呢?

4.1 ModuleMap 與 Umbrella

以基于 cocoapods 作為組件化管理工具為例,開啟 Module 之后工程上帶來最直觀的改變是pod組件下 Support Files 目錄新增幾個文件:podxxx.moduleMap , podxxx-umbrella.h。

Clang 官方文檔指出如果要支持 Module,必須提供一個 ModuleMap 文件用來描述從頭文件到模塊邏輯結構的映射關系。ModuleMap 文件的書寫使用 Module Map Language。通過示例可以發現它定義了 Module 的名字,umbrella header 包含了其目錄下的所有頭文件。module * 該通配符的作用是為每個頭文件創建一個 subModule。

簡單來說,我們可以認為 ModuleMap 文件為編譯器提供了構建 Clang Module 的一張地圖。它描述了我們要構建的 Module 的名稱以及 Module 結構中要暴露供外界訪問的 API。為編譯器構建 Module 提供必要條件。

除了上述開啟 Module 的組件會新增 ModuleMap 與 Umbrella 文件之外。在使用開啟 Module 的組件時候也有一些改變,使用 Module 組件的 target 中 BuildSetting 中 Other C Flag 中會增加 -fmodule-map-file 的參數。

蘋果官方文章中對該參數的解釋為:

Load the given module map file if a header from its directory or one of its subdirectories is loaded.

(當我們加載一個頭文件屬于 ModuleMap 的目錄或者子目錄則去加載 ModuleMap File)。

4.2 Module 的構建

了解完 ModuleMap 與 Umbrella 文件和新增的參數之后,我們決定深入去跟蹤一下這些文件與參數的在編譯期間的使用。

上文提到過在詞法分析階段以“#”開頭的預處理指令,我們對針對 HeaderName 文件進行真實路徑查找,并對要導入的文件進行同樣的詞法,語法,語義等操作。在開啟 Module 化之后,頭文件查找流程與之前有什么區別呢?在不修改代碼的基礎上編譯器又是怎么識別為語義化模型導入(Module import)呢?

如下圖所示:在初始化預處理之前,會針對 buildsetting 中設置的 Header Search path,framework Search Path 等編譯參數解析賦值給 SearchDirs。

在 Clang 的源碼中 Header Search 類負責具體頭文件的查找工作,Header Search 類中持有的 SearchDirs 存放著當前編譯文件所需要的頭文件搜索路徑。其中對于一個頭文件的搜索分三種情況:hmap, Header Search Path 以及 frameworks search path。而 SearchDirs 的賦值發生在編譯實體(CompilerInstance)初始化預處理器時,而這些參數的來源則是在 Xcode 工程 Buildsetting 中的相關編譯參數。

編譯器在查詢頭文件具體磁盤路徑的過程中,會通過 Header.h 或者 PodName/Header.h 與 SearchDirs 集合中的路徑拼接判斷該路徑下是否存在我們要查找的頭文件。當前循環的 SearchDirs 對應的元素中根據類型:(Header Search Path,frameworks,HeaderMap)進行相應的查詢流程。

上文提到過針對開啟 Module 的組件不需要額外的修改頭文件導入的代碼,編譯器自動識別我們的頭文件導入是否屬于 Module,而判斷 Header 導入是否屬于 Module import 就發生在查找頭文件路徑中。上述代碼我們會注意到針對 framework 與常規的目錄查找中,會透傳一個參數 SuggestedModule。

我們進一步向下跟蹤 SuggestModule 的賦值過程,在查找到頭文件的磁盤路徑之后,編譯器會進行該文件目錄或者父級目錄路徑作為 Key 去 UmbrellaDirs 查找該頭文件的是否有對應的 Module 存在。如果能查詢到則賦值 SuggestModule(ModuleMap::KnownHader(Module *,NormalHeader) )。下圖為查詢并賦值 SuggestModule 的流程。

相信你看到上面的源碼,你又會出現新的疑惑。UmbrellaDirs 是什么?前面提到過使用開啟 Module 組件的 Target 中會新增 -fmodule-map-file 的參數,編譯器在解析編譯參數時加載 MoudleMapFile,讀取使用 Module Map Language 書寫的 ModuleMap 文件,解析文件的內容。

編譯器在編譯工程源代碼時候通過 -fmodule-map-file 參數讀取我們要使用的 Module,并把 ModuleMap 文件所在的路徑作為 key,我們要使用的 Module 作為 Value,賦值給 UmbrellaDirs。預處理器在解析外界引入的頭文件時候,會判斷頭文件路徑下或者頭文件路徑父級目錄是否存在 ModuleMap 文件,如果存在則 SuggestModule 有值。頭文件查找的流程至此結束。

SuggestModule 的值是編譯器決定使用 Module import 還是“文本導入” 的關鍵因素。預處理器處理頭文件導入,會去查找頭文件在磁盤上的絕對路徑,如果 SuggestModule 有值,編譯器會調用 ModuleLoader 加載需要的 Module,而不開啟 Module 的組件頭文件,編譯器則會進入該文件進行新的詞法分析等流程。

至此,相信讀到這里大家對 ModuleMap、Umbrella 文件以及 -fmodule-map-path 有了一定的認知。而且我們也跟蹤了為什么編譯器可以做到不修改代碼的“智能”的幫助代碼在 # import 和 Module import 之間切換。

與非 module 不同,我們來繼續追蹤一下 LoadModule 的后續發生了什么?ModuleLoader 進行指定的 Module 的加載,而這里的 LoadModule 正是 Module 機制的差異之處。

Module 的編譯與加載是在第一次遇到 Moduleimport 類型的 importAction 時候進行緩存查找和加載,Module 的編譯依賴 moduleMap 文件的存在,也是編譯器編譯 Module 的讀取文件的入口,編譯器在查找過程中命中不了緩存,則會在開啟新的 compilerInstance,并具備新的預處理上下文,處理該 Module 下的頭文件。產生抽象語法樹然后以二進制形式持久化保存到后綴為 .pcm 的文件中(有關 pcm 文件后文有詳細講解),遇到需要 Module 導入的地方反序列化 PCM 文件中的 AST 內容,將需要的 Type 定義,聲明等節點加載到當前的翻譯單元中。

Module 持有對 Module 構建中每個頭文件的引用,如果其中任何一個頭文件發生變化,或者 Module 依賴的任何 Module 發生變化,則該 Module 會自動重新編譯,該過程不需要開發人員干預。

4.3 Clang Module 復用機制

Clang Module 機制的引入,不僅僅從之前的“文本復制”到語義化模型導入的轉變。它的設計理念同時也著重在復用機制,做到一次編譯寫入緩存 PCM 文件在此后其他的編譯實體中復用緩存。關于 Module 都是編譯和緩存探究的驗證,我們可以在 build log 中通過 -fmodules-cache-path 來查看獲取到 Module 緩存路徑(eg:/Users/xxx/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/ )。當前如果你想自定義緩存路徑可以通過添加 -fmodules-cache-path 指定緩存路徑。

我們知道針對組件化工程,我們每個 pod 庫都可能存在復雜的依賴關系,以某工程示例:

在多組件工程中,我們會發現不同的組件之間會存在相同的依賴情況。針對復雜的 Module 依賴的場景,通過 Clang源碼發現,在編譯 Module-lifeCirclePod(上述示例)時候,而 lifeCirclePod 依賴于 Module-UIKitPod。在編譯 Module-lifeCirclePod 遇到需要 Module-UIKitPod 導入時,那么此時則會掛起該編譯實體的線程,開辟新的線程進行 Module-UIKitPod 的編譯。

當 Module-UIKitPod 編譯完成時候才會恢復 lifeCirclePod 的任務。而開啟 Module 之后每個組件都會作為一個 Module 編譯并緩存,而當 MainPagePod 后續編譯過程中遇到 Module-UIKitPodModule 的導入時,復用機制就可以觸發。編譯器可以通過讀取 pcm 文件,反序列化 AST 文件直接使用。編譯器不用每次重復的去解析外界頭文件內容。

上述基本對 Module 的本質及其復用機制有一定的了解,是不是無腦開啟 Moudle 就可以了呢?

其實不然!

我們在實踐中發現(以基于 cocoapods 管理為例)在 fmodules-cache-path 的路徑下存在很多份的 pcm 緩存文件,針對同一個工程就會發現存在多個下面的現象:

可以發現在工程的一次編譯下,會出現多個目錄出現同一個 module 的緩存情況(eg:lifeCirclePod-1EBT2E5N8K8FN.pcm)。之前講過 Module 機制是一次編譯后續復用的嗎?實際情況好像與我們的理論沖突!這就要求我們去深入探究 Module 復用的機制。

追尋 Clang 的源碼發現編譯器進行預處理器 Preprocessor 的創建時,會根據自身工程的參數來設定 Module 緩存的路徑。

我們將影響 Module 緩存的產生的 hash 目錄的主要受編譯參數分為下面幾大類:

在實際的工程中,常常不同 pod 間的 build settting 不同,導致在編譯過程中會生成不同的 hash 目錄,從而緩存查找時候會出現查找不到 pcm 緩存而重復生成 Module 緩存的現象。這也解釋了我們上面發現不同的緩存 hash 目錄下會出現相同名字的 pcm 緩存。了解 Module 緩存的因素可以有助于在復雜的工程場景中,提高 Module 的復用率減少 Module Complier 的時間。

Tips:除了上述的緩存 hash 目錄外,我們會發現在目錄下存在以 ModuleName-hashxxxxxx.pcm 的命名,那么緩存文件的命名方式我們發現是 ModuleName+hash 值的方式,hash 值的生成來自 ModuleMap 文件的路徑,所以保持工程路徑的一致性也是 Module 復用的關鍵因素。

4.3 PCM

上文提到了一個很重要的文件 PCM,那么 PCM 文件作為 Module 的緩存存放,它的內容又是怎么樣的呢?

提到 PCM 文件,我們第一時間很容易聯想到 PCH。PCH 文件的應用大家應該都很熟悉,根據蘋果在介紹 PCH 的官方文檔中結構如下:

PCH 中存放著不同的模塊,每個模塊都包含 Clang 內部數據的序列化表示。采用 LLVM’s bitstream format 的方式存儲。其中 metadata 塊主要用于驗證 AST 文件的使用;SourceManager 塊它是前端 SourceManager 類的序列化,它主要用來維護 SourceLocation 到源文件或者宏實例化的實際行/列的映射關系;Types: 包含 TranslationUnit 引用的所有類型的序列化數據,在 Clang 類型節點中,每個節點都有對應的類型;Declarations: 包含 TranslationUnit 引用的所有聲明的序列化表示;Identifier Table: 它包含一個 hash Table,該表記錄了 ASTfile 中每個標識符到標識符信息的序列化表示;Method Pool: 它與 Identifier Table 類似,也是 Hash Table,提供了 OC 中方法選擇器和具體類方法和實例方方法的映射。Module 實現機制與 PCH 相同,也是序列化的 AST 文件,我們可以通過 llvm-bcanalyzer 把 pcm 文件的內容 dump 出來。

Module 的編譯是在獨立的線程,獨立的編譯實體過程,與我們輸出目標文件對應的前端 action 不同,它所對應的FrontAction為GenerateModuleAction。Module 的機制思想主要是提供一種語義化的模塊導入方式。所以 PCM 的緩存內容同樣會經過詞法,語法,語義分析的過程,PCM 文件中的 AST 模塊的序列化保存是在發現在語義分析之后。

它利用了 Clang AST 基類中的 ASTConsumer 類,該類提供了若干可以 override 的方法,用來接收 AST 解析過程中的回調,當編譯單元TranslationUnit的AST完整解析后,我們可以通過調用 HandleTranslationUnit 在獲取到完整抽象語法樹上的所有節點。PCM 文件的寫入由 ASTWriter 類提供 API,這些具體的流程我們可以在 ASTWriter 類中具體跟蹤。在該過程中主要分為 ControlBlock 信息的寫入,該步驟包含 metadata, InputFiles,Header search path 等信息的記錄。這些 PCM 的具體內容 dump 出來如下圖:

其中 Types,Declarations 等信息的寫入流程發生在 ASTBlock 階段。由于在處理處理 ModuleMap 文件的編譯流程中會對 umbrella.h 中所暴露的頭文件進行預處理,詞法,語法,語義分析等流程。我們在使用 WriteAST 寫入時,會將當前編譯實體的 Sema 類(該類是 build AST 和語義分析的實現類)傳遞過來。Sema 持有當前的 ASTContext,ASTContext 則可以用于訪問當前抽象語法樹上的所有 Nodes(例如 types,decls)等信息。

如果所示:ASTWriter 將已經解析無誤的 Module 信息,包括 AST 等內容寫入 Module 的緩存文件 PCM 中。

我們在源碼跟蹤過程中可以發現會將AST節點信息等寫入PCM中的ASTBlock中,我們可以通過打印獲取到節點的類型和節點的名稱:

通過上面源碼等流程相信你掌握了以下:

ModuleMap 文件用來描述從頭文件到模塊邏輯結構的映射關系,Umbrella 或者Umbrella Header 描述了子Module的概念;

Module 的構建是“獨立”進行的,Module 間存在依賴時,優先編譯完成被依賴的Module;

Clang 提供了 Module 的新用法(@import ModuleName),但是針對就項目無需改造,Clang 在預處理時期提供了 Module 與非 Module 的轉換;

Module 提供了復用的機制,它將暴露外界的 API 以 ASTFile 格式存儲,在代碼未發生變化時,直接讀取緩存。而在代碼變動時,Xcode 會在合適的時機對 Module 進行更新,開發者無需額外干預。

同城編譯時間數據分析

鑒于在58同城工程上實施的編譯數據時間的加長的背景,我們在深入探究 Module 構建,復用等機制后,我們針對整個編譯流程做了詳細的編譯階段的插樁。

5.1 分析工具

Clang 9.0 合并了一個非常有用的功能 -ftime-trace,該功能允許以友好的格式生成時間跟蹤分析數據,clang中預先插入了一些點標記,如每個文件的編譯時間ExecuteCompiler、前端編譯時間Frontend、module加載時間Module Load、后端處理時間Backend等。接下來通過-ftime-trace查看各編譯階段的打點時間。操作比較簡單,只需要在Other C Flags中添加-ftime-trace即可。

編譯完成后clang會在編譯目錄下,為每個源文件自動生成一個json文件,文件名和源碼文件相同。

每個json文件中大概會有ExecuteCompiler、Frontend、Source、Module Load、Backend等打點數據,也有Total ExecuteCompiler、Total Frontend、Total Source、Total Module Load、Total Backend這樣的數據,后者是前者的一個匯總,這是clang自帶的,也可以在clang中去擴展。通過chrome://tracing/可以很方便查看單個json文件的耗時分布,如下。

-ftime-trace設置后主要時間段說明:

Total ExecuteCompiler:文件編譯總時間;

Total Frontend:前端編譯時間,如在clang中編譯時間;

Total Source:頭文件處理時間,如處理import;

Total Module Load:Module的加載時間,如在Source的處理過程中,判斷當前import的是一個module,則會執行此操作,如import系統庫;

Total Module Compile:Module的編譯時間,如第一次加載自定義的源碼Module,會對Module進行編譯,生成AST緩存起來;

Total Backend:編譯器后端處理時間。

這些時間段都是Clang中已有的打點,從前面的chrome://tracing/圖也能看出來是有一些包含關系的,如:

    ExecuteCompiler 包含Frontend和Backend;

    Frontend包含Source;

    Source中包含Module Load(前提是如當前.m中import了A/XX.h,而A沒有module化,但XX.h中import了B/YY.h,B是Module化的,如果A是module化的,Module Load不包含在Source中);

    Module Load包含Module Compile。

5.2 時間段分析

先選取單個文件進行分析,將其拖到chrome://tracing/中,可看到如下數據。

從圖上可看出,Total Frontend占總編譯時間在都在70%以上,module編譯中Total Frontend時間比非module明顯要長,而Total Source占Total Frontend時間的70%左右,而Total Module Load是Total Source中最耗時的操作。結果中Total Module Load階段,module明顯是要比非module耗時更長。

上面是從單個文件進行分析,并不能代表整體項目的編譯情況,因此,我們做了一個自動化工具,將所有.json文件中的對應時間進行統計匯總,得出整體各個時間段的匯總數據,如下。說明一下,我們統計的Total ExecuteCompiler指每個文件的編譯時間總和,相當于在單核下編譯時間,而前面顯示的實際整體的編譯時間少很多,是因為我們實際是在多核下編譯。

從整體分析圖上可看出,Total Frontend時間均占總編譯時間Total ExecuteCompiler的80%以上,而Total Frontend中時間Total Source的總時間占80%以上,而在Total Source中Total Module Load時間占70%左右。總時間Total ExecuteCompiler和前端Total Frontend依然是module下更長,而在Total Frontend中Total Module Load的時長在module下明顯比非module下長很多,跟上面單文件分析的結論基本一致。這里需要注意的是,Total ExecuteCompiler時間比前面統計的總時間長很多,是因為項目是在多核下編譯,而Total ExecuteCompiler統計的是所有文件編譯時間總和,而前面統計的時間是多文件并行編譯下的時間,其它各段時間同理。

在Total Module Load中會執行Module的編譯,但從上圖我們可以看到其實Total Module Compile時間很短,都不超過50S,因此還需要進一步分析Total Module Load的耗時操作。為此我們根據clang中的處理流程,在clang中Module Load處理代碼中擴展兩個打點:

Module ReadAST:驗證Module緩存并反序列化Module cache PCM文件的時長;

Module WaitForLock:一個線程在ModuleCompiler期間,其他線程需要掛起等待的時長。

并在頭文件查找擴展打點:

Lookup HeaderFile :預處理階段查找導入頭文件的磁盤路徑時間。

將Clang源碼修改后編譯生成自定義的Clang,替換XCode中的Clang分別在module和非module下再次進行編譯,得出如下數據:

從圖中可以看出,Module Load階段中Module ReadAST時間占比近70%,此次編譯module比非module下時間長約3%,而Module ReadAST段module比非module下時間長約2%,整個Module Load階段module下比非module下長約4%。

因此,我們可以得出,相比非module,module化編譯更為耗時,而主要耗時在驗證Module緩存并反序列化操作。那么問題來了,有什么辦法可以在module開啟的情況下進行編譯時間優化呢?

編譯時間的優化

從上面的數據分析我們知道,如果底層組件進行 Module 化,并且上層組件通過module方式進行引用的話,會更耗時。但是為了支持 Swift/OC 混編,如 Swift 調用 OC,需要對組件進行 Module 化。因此,我們需要在 Module 化的基礎上優化編譯時間,如果上層組件不通過 Module 方式調用其它 Module 化的組件,而采用非 Module 化方式進行引用,理論上是能避免上述module化操作的耗時。

6.1 優化方案

為了進一步優化混編下的編譯時間,我們參考蘋果 WWDC 2018 的 header search path 中 headermap 查找方案,主要思路是通過 hmap 的方式來替換header search path 下的文件搜索,來減少編譯耗時,為描述方便,我們稱為hmap方案,目前業內美團對 hmap 有應用,并且有 50% 的優化效果。58同城也對 headermap 方案進行了研究并進行了落地,理想的實現方案就是做一個 cocoapods 插件,在插件中做了以下幾件事:

    HooksManager注冊cocoapods的post_install鉤子;

    通過header_mappings_by_file_accessor遍歷所有頭文件和header_dir,由header_dir/header.h和header.h為key,以頭文件搜索路徑為value,組裝成一個Hash<key,value>,生成所有組件pod頭文件的json文件,再通過hmap工具將json文件轉成hmap文件。

    再修改各pod中.xcconfig文件的HEADER_SEARCH_PATHS值,僅指向生成的hmap文件,刪除原來添加的搜索目錄;

    修改各pod的USE_HEADERMAP值,關閉對默認的hmap文件的訪問。

58對應的插件名為cocoapods-wbhmap,插件完成后,在Podfile中通過plugin 'cocoapods-wbhmap'接入。

6.2 優化數據

以下是58同城分別在非 Module、Module 化和優化后的 hmap 三種場景下編譯時間數據,這里的 hmap 是在各組件 Module 化的基礎上使用的。

首先說明一下,這里的整體編譯時間數據上跟前面不一致,是因為重新編譯了,每次編譯時間略有不同,但不影響我們分析。從整體時間來看 Module 下的編譯時間比非 Module 下略長,而 hmap 比非 Module 下優化了 32% 左右,比 Module 下優化了 33% 左右,可以看出 hmap 的優化效果是很顯著的。

接下來分析一下編譯各階段的時間,是不跟我們預想的一致,我們預想的是 Total Lookup HeaderFile 和 hmap 在 Module Load 階段加載的 Module基本是系統庫,應當時間上差不多,而由于hmap節省了在眾多目錄下文件搜索的時間,應當在Total Lookup HeaderFile有較大差別。

從分段數據來看,三種編譯方式的 Total ExecuteCompiler 跟上述整體時間比例接近,但是 Total Lookup HeaderFile 時間都較小,自然沒多大差別,而 Total Module Load 差別較大,非 Module 和 Module 下比 hmap 大 61% 左右,跟我們預想的不一致。觀察數據可以看到,Module Load 中大部分時間是在 Module ReadAST 階段,因而我們繼續研究 Module ReadAST 中的處理操作。

6.3 hmap 優化了什么?

針對 ReadAST 階段再次細分打點計時,發現在 ReadAST 階段去讀取緩存時候,會對緩存 PCM 文件的 ControlBlock 塊信息進行解析,該內容包含了當前 Module 緩存引用外界其他 ASTFile 的記錄。而加載外界 ASTFile 的 PCM 緩存時候,會針對該 ModuleName 進行驗證確保我們不會加載一個 non-Module 的 ASTFile 作為一個 Module。它通過查詢是否存在 ModuleMap 文件來描述 Module 對應當前要查詢的 ModuleName。

我們將重點聚焦在這個階段,因為我們 hmap 方案最直接的優化之處在減少了 Header Search Path 的參數路徑,將預處理期間的頭文件查找轉換為 key-value 查找,從而減少了在 Header Search Path 眾多 pod 的目錄中(如private、public)的搜索時間,源碼中 SearchDirs 即為這些目錄,Header Search Path 中目錄越多,SearchDirs 中元素更多,要遍歷的目錄就更多,無用的搜索時間就越長,通過單個文件進行調試發現這里消耗的時間約有 70%,而系統庫的查找在這里耗時較長,因為按照編譯器搜索的順序,系統庫目錄的是排在 Header Search Path 后的,經過一頓徒勞的搜索之后才到系統庫目錄搜索,效率較低。

我們猜想前面非 Module 和 hmap 在 Module Load 時間差較大的原因應當就在此,因此在 ReadAST 階段的 HeaderSearch::lookupModule 方法內打個點 Lookup Module,即 Module ReadAST 包含 Lookup Module,重新編譯進行數據統計如下:

這里只統計非 Module 和 hmap,整體編譯時間如下:

從數據可以看出,再次編譯 hmap 下的編譯時間比非 Module 方式同樣是優化了 35% 左右。再看分段數據,如下:

從占比分析,非 Module 方式下 Total Lookup Module 時間占 Total Module ReadAST 時間的 77%,并占 Total Module Load 時間的 72%,而在 hmap 方式中,Total Lookup Module 時間占 Total Module ReadAST 時間的 35%,并占 Total Module Load 時間的 27%,遠小于非 Module 方式下的占比。

從數值分析,非 Module 方式下 Total Lookup Module 時間為 1422 秒,而 hmap 方式下時間僅為 182 秒,相差 7 倍多。

上面數據也進一步驗證了我們對于 hmap 編譯時間優化原因的猜想。到這里我們就從數據和原理上對 hmap 方案的編譯優化做了一個完整的分析。

總結

由于 Swift/OC 混編項目的需要,58同城對組件進行了 Module 化,并且嘗試讓所有組件通過 Module 方式進行頭文件引用。但我們發現編譯時間卻比非 Module 情況下更長,這也與蘋果官方在 WWDC2013 中的 Module 性能分析結果不符。

然后在尋求編譯時間的優化方案時,發現在 WWDC2018 中有提到 hmap 機制,并借鑒業內的一些寶貴經驗,采用了 hmap 方案對編譯時間進行優化。Module 方案雖無法降低編譯耗時,但對比之前混編的橋接方式,可增強項目向 Swift 遷移過程中混編組件的可維護性。通過 hmap 方案對編譯時間進行優化,同城最終編譯時間比 Module 化之前優化了約 35%,對于其它 App 的 Module 化也是有較好的借鑒意義。

作者簡介

趙志:58同城-用戶價值增長部

曾慶隆:58同城-用戶價值增長部

顧夢奇:58同城-房產事業群

王強:58同城-招聘客戶端

趙發:58同城-汽車事業群

參考文獻

LLVM源碼:github/llvm/llvm-project

Clang/LLVM官方文檔:clang.llvm.org/docs/

蘋果WWDC 2013 Advances in Objective-C Module相關視頻:developer.apple/videos/play/wwdc2013/404/

蘋果WWDC 2018 Header Search Path相關視頻:developer.apple/videos/play/wwdc2018/415/

LLVM開發者大會Doug Gregor的視頻和PPT:llvm.org/devmtg/2012-11/

ftime-trace耗時報告配置:blog.csdn/wwchao2012/article/details/109147192

美團編譯速度優化公眾號文章:mp.weixin.qq/s?__biz=MjM5NjQ5MTI5OA==&mid=2651760497&idx=1&sn=2042896ac13cbc9b010625c7c24897e8&chksm=bd127e3c8a65f72aab2f2e0993654593bfbe4c44db36709f909ae40ce69cb0c2e02598c0ebc0&cur_album_id=1751291735726456834&scene=189#rd

Hmap工具:github/milend/hmap

llvm-bcanalyzer:llvm.org/docs/CommandGuide/llvm-bcanalyzer.html

bitstream format:llvm.org/docs/BitCodeFormat.html

PCH結構:clang.llvm.org/docs/PCHInternals.html#pchinternals-modules

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

 
(文/企資小編)
免責聲明
本文為企資小編推薦作品?作者: 企資小編。歡迎轉載,轉載請注明原文出處:http://www.bangpiao.com.cn/news/show-172440.html 。本文僅代表作者個人觀點,本站未對其內容進行核實,請讀者僅做參考,如若文中涉及有違公德、觸犯法律的內容,一經發現,立即刪除,作者需自行承擔相應責任。涉及到版權或其他問題,請及時聯系我們郵件:weilaitui@qq.com。
 

Copyright ? 2016 - 2023 - 企資網 48903.COM All Rights Reserved 粵公網安備 44030702000589號

粵ICP備16078936號

微信

關注
微信

微信二維碼

WAP二維碼

客服

聯系
客服

聯系客服:

在線QQ: 303377504

客服電話: 020-82301567

E_mail郵箱: weilaitui@qq.com

微信公眾號: weishitui

客服001 客服002 客服003

工作時間:

周一至周五: 09:00 - 18:00

反饋

用戶
反饋

主站蜘蛛池模板: 国产三级图片 | 欧美日韩中文 | 日韩一级欧美一级在线观看 | 国产精品香蕉成人网在线观看 | 色天天综合色天天天天看大 | 曰批全过程免费视频播放网站 | 美女福利视频导航 | 在线视频免费观看a毛片 | 热久久视久久精品18国产 | 欧美国产成人精品一区二区三区 | 欧美乱妇视频 | 狂野欧美性猛交xxxx免费按摩 | 最新黄色网址在线观看 | 日韩伦 | 精品午夜一区二区三区在线观看 | 亚洲黄色片视频 | 97影院理伦在线观看 | 91欧美在线 | 激情影院成人区免费观看视频 | 天天射天天操天天干 | 亚洲欧美二区三区久本道 | 操操干干| 欧美日韩在线观看视频 | 国产精品成人一区二区 | 久久国产欧美日韩高清专区 | 又黄又刺激视频 | 国产日韩一区在线精品欧美玲 | 91精品国产三级在线观看 | 午夜视频国语 | 久久国产精品-国产精品 | 99艾草视频在线播放 | 国产成人免费影片在线观看 | 日本不卡视频在线 | 国产日韩欧美中文字幕 | aaaaaaa毛片| 日韩欧美理论 | 欧美午夜性春猛交 | 韩国理伦伦片在线观看 | 国产第一区二区三区在线观看 | 国产成人精品视频一区 | 色播在线永久免费视频 |