国产高清吹潮免费视频,老熟女@tubeumtv,粉嫩av一区二区三区免费观看,亚洲国产成人精品青青草原

二維碼
企資網(wǎng)

掃一掃關(guān)注

當(dāng)前位置: 首頁(yè) » 企資頭條 » 頭條 » 正文

Swift_與_Objective_C_混編

放大字體  縮小字體 發(fā)布日期:2021-09-05 02:29:59    作者:企資小編    瀏覽次數(shù):50
導(dǎo)讀

作者 | 趙志、曾慶隆、顧夢(mèng)奇、王強(qiáng)、趙發(fā)出品 | CSDN(ID:CSDNnews)2019 年 3 月 25 日,蘋(píng)果發(fā)布了 Swift 5.0 版本,宣布了 ABI 穩(wěn)定,并且Swift runtime 和標(biāo)準(zhǔn)庫(kù)已經(jīng)植入系統(tǒng)中,而且蘋(píng)果新出文檔都用 Swift,

作者 | 趙志、曾慶隆、顧夢(mèng)奇、王強(qiáng)、趙發(fā)

出品 | CSDN(ID:CSDNnews)

2019 年 3 月 25 日,蘋(píng)果發(fā)布了 Swift 5.0 版本,宣布了 ABI 穩(wěn)定,并且Swift runtime 和標(biāo)準(zhǔn)庫(kù)已經(jīng)植入系統(tǒng)中,而且蘋(píng)果新出文檔都用 Swift,Sample Code 也是 Swift,可以看出 Swift 是蘋(píng)果扶持與研發(fā)的重點(diǎn)方向。

目前國(guó)內(nèi)外各大公司都在相繼試水,只要關(guān)注 Swift 在國(guó)內(nèi) iOS 生態(tài)圈現(xiàn)狀,你就會(huì)發(fā)現(xiàn),Swift 在國(guó)內(nèi) App 應(yīng)用的比重逐漸升高。對(duì)于新 App 來(lái)說(shuō),可以直接用純 Swift 進(jìn)行開(kāi)發(fā),而對(duì)于老 App 來(lái)說(shuō),絕大部分以前都是用 OC 開(kāi)發(fā)的,因此 Swift/OC 混編是一個(gè)必然面臨的問(wèn)題。

CSDN 付費(fèi)下載自視覺(jué)中國(guó)

Swift 和 OC 混編開(kāi)發(fā)

關(guān)于 Swift 和 OC 間如何混編,業(yè)內(nèi)也已經(jīng)有很多相關(guān)文章詳細(xì)講解,簡(jiǎn)單來(lái)說(shuō) OC/Swift 調(diào)用 Swift,最終通過(guò) Swift Module 進(jìn)行,而 Swift 調(diào)用 OC 時(shí),則是通過(guò) Clang Module,當(dāng)然也可以通過(guò) Clang Module 進(jìn)行 OC 對(duì) OC 的調(diào)用。58同城于 2020 年正式上線首個(gè) Swift/OC(Objective-C,以下簡(jiǎn)稱 OC)項(xiàng)目,與此同時(shí),也在全公司范圍內(nèi)開(kāi)展了一個(gè)多部門(mén)協(xié)作項(xiàng)目——混天項(xiàng)目,主要目標(biāo):

一是提供混編的基礎(chǔ)設(shè)施建設(shè),如提供通過(guò)的 Module 化方案;

二是擴(kuò)展各工具鏈的混編能力,如對(duì)無(wú)用類檢測(cè)工具 WBBlades(github/wuba/WBBlades)進(jìn)行 Swift 能力的擴(kuò)展;

三是對(duì)已有的基礎(chǔ)庫(kù)進(jìn)行 Module 化和 Swift 適配;

四是將混編開(kāi)發(fā)在各 App 和各業(yè)務(wù)線中推廣和落地。

我們?cè)?Module 化實(shí)踐中發(fā)現(xiàn),實(shí)際數(shù)據(jù)與蘋(píng)果官方 Module 編譯時(shí)間數(shù)據(jù)不一致,于是我們通過(guò) Clang 源碼和數(shù)據(jù)相結(jié)合的方式對(duì) Clang Module進(jìn)行了深入研究,找到了耗時(shí)的原因。由于 Swift/OC 混編下需要 Module 化的支持,同時(shí)借鑒業(yè)內(nèi) HeaderMap 方案讓 OC 調(diào)用 OC 時(shí)避開(kāi) Module 化調(diào)用,將編譯時(shí)間優(yōu)化了約 35%,較好地解決了在 Module 化下的編譯時(shí)間問(wèn)題。

Clang Module 初探

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

一個(gè)源代碼文件到經(jīng)過(guò)編譯輸出為目標(biāo)文件主要分為下面幾個(gè)階段:

源文件在經(jīng)過(guò) Clang 前端包含:詞法分析(Lexical analysis) 、語(yǔ)法分析(Syntactic analysis) 、語(yǔ)義分析(Semantic analysis)。最后輸出與平臺(tái)無(wú)關(guān)的 IR(LLVM IR generator)進(jìn)而交給后端進(jìn)行優(yōu)化生成匯編輸出目標(biāo)文件。

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

2.1 普通 import 的機(jī)制

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

針對(duì)編譯器遇到 #import<PodName/header.h> 或者 #import ”header.h” 這種導(dǎo)入方式時(shí)候,# 開(kāi)頭在詞法分析階段會(huì)觸發(fā)預(yù)處理(Preprocessor)。而對(duì)于 Clang 的預(yù)處理器 import 與 include 指令都屬于它的關(guān)鍵詞。預(yù)處理器在處理 import Directive 時(shí)候主要工作為通過(guò)導(dǎo)入的 header 名稱去查找文件的磁盤(pán)所在路徑,然后進(jìn)入該文件創(chuàng)建新的詞法分析器對(duì)導(dǎo)入的頭文件進(jìn)行詞法分析。

如下所示:編譯器在遇到 #import 或者 #include 指令時(shí),觸發(fā)預(yù)處理機(jī)制查詢頭文件的路徑,進(jìn)入頭文件對(duì)頭文件的內(nèi)容進(jìn)行解析的流程。

以單個(gè)文件編譯過(guò)程為維度舉例:在針對(duì)一個(gè)文件編譯輸出目標(biāo)文件的過(guò)程中,可能會(huì)引入多個(gè)外界的頭文件,而被引入多個(gè)外界頭文件也有可能存在引入外界頭文件。這樣的情況就導(dǎo)致雖然只是在編譯單個(gè)文件,但是預(yù)處理器會(huì)對(duì)引入的頭文件進(jìn)行層層展開(kāi)。這也是很多人稱 #import 與 include 是一種特殊“復(fù)制”效果的原因。

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

來(lái)看下面一段代碼,在 PodBTestObj 類的文件中定義一個(gè) ClassName 字符串的宏,然后在導(dǎo)入 PoBClass1.h 頭文件,在 PoBClass1.h 的頭文件中同樣定義一個(gè)結(jié)構(gòu)體名為 ClassName,這里與我們?cè)?PodBTestObj 類中定義的宏同名。預(yù)處理的特殊的“復(fù)制”機(jī)制,在預(yù)處理階段會(huì)發(fā)生下圖所見(jiàn)的結(jié)果:

這樣的問(wèn)題相信在日常開(kāi)發(fā)中并不罕見(jiàn),而為了解決這種重名的問(wèn)題,我們常規(guī)的手法只能通過(guò)增加前綴或者提前約定規(guī)則等方式來(lái)解決。

視頻中同時(shí)指出這種機(jī)制在應(yīng)對(duì)大型工程編譯過(guò)程中的所帶來(lái)的消耗問(wèn)題。假設(shè)有 N 個(gè)源文件的工程,那么每個(gè)源文件引用 M 個(gè)頭文件,由于預(yù)處理的這種機(jī)制,我們?cè)卺槍?duì)處理每個(gè)源文件的編譯過(guò)程中會(huì)對(duì)引入的 M 個(gè)頭文件進(jìn)行展開(kāi),經(jīng)歷一遍遍的詞法分析-語(yǔ)法分析-語(yǔ)義分析的過(guò)程。那么你能想象一下針對(duì)系統(tǒng)頭文件的引入在預(yù)處理階段將會(huì)是一個(gè)多么龐大的開(kāi)銷!

那么針對(duì) C 語(yǔ)言預(yù)處理器存在的問(wèn)題,蘋(píng)果有哪些方案可以優(yōu)化這些存在的問(wèn)題呢?

2.2 PCH (Precompiled Headers)

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

關(guān)于 PCH 的概述,蘋(píng)果是這樣定義的:

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

(使用 Clang 內(nèi)部數(shù)據(jù)結(jié)構(gòu)序列化表示,采用的 LLVM 字節(jié)流表示)。

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

存儲(chǔ)內(nèi)容方面它存放著序列化的 AST 文件。AST 文件本身包含 Clang 的抽象語(yǔ)法樹(shù)和支持?jǐn)?shù)據(jù)結(jié)構(gòu)的序列化表示,它們使用與 LLVM’s bitcode file format. 相同的壓縮位流進(jìn)行存儲(chǔ)。關(guān)于 AST File 文件的存儲(chǔ)結(jié)構(gòu)你可以在官方文檔有詳細(xì)的了解。

它作為蘋(píng)果一種優(yōu)化方案被提出,但是實(shí)際的工程中源代碼的引用關(guān)系是很復(fù)雜的,所以找出一組幾乎所有源文件都包含的頭文件基本不可能,同時(shí)對(duì)于代碼更新維護(hù)更是一個(gè)挑戰(zhàn)。其次在被包含頭文件改動(dòng)下,因?yàn)?PCH 會(huì)被所有源文件引入,會(huì)帶來(lái)代碼“污染”的問(wèn)題。同時(shí)一旦 PCH 文件發(fā)生改動(dòng),會(huì)導(dǎo)致大面積的源代碼重編造成編譯時(shí)間的浪費(fèi)。

2.3 Modules

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

那么 Module 到底是什么呢?

Module 簡(jiǎn)單來(lái)說(shuō)可以認(rèn)為它是對(duì)一個(gè)組件的抽象描述,包含組件的接口和實(shí)現(xiàn)。Module 機(jī)制推出主要用來(lái)解決上述所闡述的預(yù)處理問(wèn)題,想要探究 Clang Module 的實(shí)現(xiàn),首先需要去開(kāi)啟 Module。那么針對(duì) iOS 工程怎么開(kāi)啟 Module 呢? 只需要打開(kāi)編譯選項(xiàng)中:

對(duì)!你沒(méi)看錯(cuò),僅僅需要在 Xcode 的編譯選項(xiàng)中修改配置即可。

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

2.4 蘋(píng)果對(duì) Module 的解讀

上文提到過(guò)基于 C 語(yǔ)言預(yù)處理器提供的 #include 機(jī)制提供的訪問(wèn)外界庫(kù) API 的方式存在的伸縮性和健壯性的問(wèn)題。Modules 提供了更為健壯,更高效的語(yǔ)義模型來(lái)替換之前 textual preprocessor 改進(jìn)對(duì)庫(kù)的 API 訪問(wèn)方式。

蘋(píng)果官方文檔中針對(duì) Module 的解讀有以下幾個(gè)優(yōu)勢(shì):

擴(kuò)展性:每個(gè) Module 只會(huì)編譯一次,將 Module 導(dǎo)入 Translantion unit 的時(shí)間是恒定的。對(duì)于庫(kù) API 的訪問(wèn)只會(huì)解析一次,將 #include 的機(jī)制下的由 M x N 編譯問(wèn)題簡(jiǎn)化為 M + N。

健壯性:每個(gè) Module 作為一個(gè)獨(dú)立的實(shí)體,具備一個(gè)一致的預(yù)處理環(huán)境。不需要去添加下劃線,或者前綴等方式解決命名的問(wèn)題。每個(gè)庫(kù)不會(huì)影響另外一個(gè)庫(kù)的編譯方式。

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

對(duì) Clang Module 機(jī)制建立一定的認(rèn)知上,我們著手進(jìn)行了 Clang Module 在 58同城 App 上的 Module 化改造。

58同城初步實(shí)踐

3.1 Module 化工程配置

組件 Module 化

在多 pod 的項(xiàng)目中,通過(guò)以下幾種方式可以將各 pod 進(jìn)行 Module 化:

    Podfile 中添加 use_modular_headers! 對(duì)所有的 pod 進(jìn)行 Module 化;

    Podfile 中通過(guò) modular_headers 對(duì)每個(gè) pod 單獨(dú)進(jìn)行 Module 化,如對(duì) PodC 進(jìn)行 Module 化,pod 'PodC', :path => '../PodC',:modular_headers => true;

    在 pod 所對(duì)應(yīng)的 .podspecs 中的 xcconfig 中 sg 配置 DEFINES_MODULE,如 s.xcconfig = {'DEFINES_MODULE' => 'YES'}。

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

在58同城中,維護(hù)了一個(gè)全局的依賴配置文件 dependency.json,這個(gè)文件通過(guò)自動(dòng)化工具進(jìn)行維護(hù),各組件 pod 的 .podspecs 從 dependency.json 中動(dòng)態(tài)讀取自己依賴的其它組件,并生成相應(yīng)的 dependency 關(guān)系。

3.2 Swift/OC 混編橋接文件

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

這樣可以達(dá)到編譯的目的,但是由于依賴的組件都是在橋接文件中統(tǒng)一配置,對(duì)于每個(gè) Swift 文件依賴了哪些 pod 組件,實(shí)際上并不清楚,而且 Swift 中每次修改新增一個(gè) OC 文件的引用,都需要在橋接文件中進(jìn)行修改,并且如果是減少對(duì)某個(gè) OC 文件的引用,也不好確定是否要在橋接文件中進(jìn)行刪除,因?yàn)檫€需要判斷其它 Swift 文件中是否有引用。

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

3.3 同城的 Module 化編譯數(shù)據(jù)

萬(wàn)事具備,只差編譯!

結(jié)合蘋(píng)果官方給出了性能數(shù)據(jù),我們預(yù)測(cè) Module 化后的編譯速度是要比非 Module 情況更快,那不妨就編譯試試,接下來(lái)在 58同城中分別在 module 和非 module 場(chǎng)景下進(jìn)行編譯。

通過(guò)編譯數(shù)據(jù),我們看到的結(jié)果發(fā)生了逆轉(zhuǎn),Module 化之后的時(shí)間竟然比非 Module 情況下長(zhǎng)約 8%,這跟剛才我們看到的蘋(píng)果官方數(shù)據(jù)不符,有點(diǎn)亂了。需要說(shuō)明的是這份數(shù)據(jù)是 58同城全業(yè)務(wù)線在 M1 機(jī)器上運(yùn)行出來(lái)的,并且把資源復(fù)制的環(huán)節(jié)從配置中刪除了,即不包含資源復(fù)制時(shí)間,是純代碼編譯時(shí)間,并且在非 M1 機(jī)器上也運(yùn)行了進(jìn)行對(duì)比,除了時(shí)間長(zhǎng)些,結(jié)論基本也是 module 化之后時(shí)間長(zhǎng) 10% 左右。

在面對(duì)實(shí)際測(cè)試結(jié)果 Module 化之后的編譯耗時(shí)更長(zhǎng)的情況下,我們從更深層次上進(jìn)行對(duì) Clang Module 原理進(jìn)行了探究。

Clang Module 原理深究

Clang Module 機(jī)制的引入主要是為了解決預(yù)處理器的各種問(wèn)題,那么工程在開(kāi)啟 Module 之后,工程上會(huì)有哪些變化呢?同時(shí)在編譯過(guò)程中編譯器工作流程與之前又有哪些不同呢?

4.1 ModuleMap 與 Umbrella

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

Clang 官方文檔指出如果要支持 Module,必須提供一個(gè) ModuleMap 文件用來(lái)描述從頭文件到模塊邏輯結(jié)構(gòu)的映射關(guān)系。ModuleMap 文件的書(shū)寫(xiě)使用 Module Map Language。通過(guò)示例可以發(fā)現(xiàn)它定義了 Module 的名字,umbrella header 包含了其目錄下的所有頭文件。module * 該通配符的作用是為每個(gè)頭文件創(chuàng)建一個(gè) subModule。

簡(jiǎn)單來(lái)說(shuō),我們可以認(rèn)為 ModuleMap 文件為編譯器提供了構(gòu)建 Clang Module 的一張地圖。它描述了我們要構(gòu)建的 Module 的名稱以及 Module 結(jié)構(gòu)中要暴露供外界訪問(wèn)的 API。為編譯器構(gòu)建 Module 提供必要條件。

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

蘋(píng)果官方文章中對(duì)該參數(shù)的解釋為:

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

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

4.2 Module 的構(gòu)建

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

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

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

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

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

上文提到過(guò)針對(duì)開(kāi)啟 Module 的組件不需要額外的修改頭文件導(dǎo)入的代碼,編譯器自動(dòng)識(shí)別我們的頭文件導(dǎo)入是否屬于 Module,而判斷 Header 導(dǎo)入是否屬于 Module import 就發(fā)生在查找頭文件路徑中。上述代碼我們會(huì)注意到針對(duì) framework 與常規(guī)的目錄查找中,會(huì)透?jìng)饕粋€(gè)參數(shù) SuggestedModule。

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

相信你看到上面的源碼,你又會(huì)出現(xiàn)新的疑惑。UmbrellaDirs 是什么?前面提到過(guò)使用開(kāi)啟 Module 組件的 Target 中會(huì)新增 -fmodule-map-file 的參數(shù),編譯器在解析編譯參數(shù)時(shí)加載 MoudleMapFile,讀取使用 Module Map Language 書(shū)寫(xiě)的 ModuleMap 文件,解析文件的內(nèi)容。

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

SuggestModule 的值是編譯器決定使用 Module import 還是“文本導(dǎo)入” 的關(guān)鍵因素。預(yù)處理器處理頭文件導(dǎo)入,會(huì)去查找頭文件在磁盤(pán)上的絕對(duì)路徑,如果 SuggestModule 有值,編譯器會(huì)調(diào)用 ModuleLoader 加載需要的 Module,而不開(kāi)啟 Module 的組件頭文件,編譯器則會(huì)進(jìn)入該文件進(jìn)行新的詞法分析等流程。

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

與非 module 不同,我們來(lái)繼續(xù)追蹤一下 LoadModule 的后續(xù)發(fā)生了什么?ModuleLoader 進(jìn)行指定的 Module 的加載,而這里的 LoadModule 正是 Module 機(jī)制的差異之處。

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

Module 持有對(duì) Module 構(gòu)建中每個(gè)頭文件的引用,如果其中任何一個(gè)頭文件發(fā)生變化,或者 Module 依賴的任何 Module 發(fā)生變化,則該 Module 會(huì)自動(dòng)重新編譯,該過(guò)程不需要開(kāi)發(fā)人員干預(yù)。

4.3 Clang Module 復(fù)用機(jī)制

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

我們知道針對(duì)組件化工程,我們每個(gè) pod 庫(kù)都可能存在復(fù)雜的依賴關(guān)系,以某工程示例:

在多組件工程中,我們會(huì)發(fā)現(xiàn)不同的組件之間會(huì)存在相同的依賴情況。針對(duì)復(fù)雜的 Module 依賴的場(chǎng)景,通過(guò) Clang源碼發(fā)現(xiàn),在編譯 Module-lifeCirclePod(上述示例)時(shí)候,而 lifeCirclePod 依賴于 Module-UIKitPod。在編譯 Module-lifeCirclePod 遇到需要 Module-UIKitPod 導(dǎo)入時(shí),那么此時(shí)則會(huì)掛起該編譯實(shí)體的線程,開(kāi)辟新的線程進(jìn)行 Module-UIKitPod 的編譯。

當(dāng) Module-UIKitPod 編譯完成時(shí)候才會(huì)恢復(fù) lifeCirclePod 的任務(wù)。而開(kāi)啟 Module 之后每個(gè)組件都會(huì)作為一個(gè) Module 編譯并緩存,而當(dāng) MainPagePod 后續(xù)編譯過(guò)程中遇到 Module-UIKitPodModule 的導(dǎo)入時(shí),復(fù)用機(jī)制就可以觸發(fā)。編譯器可以通過(guò)讀取 pcm 文件,反序列化 AST 文件直接使用。編譯器不用每次重復(fù)的去解析外界頭文件內(nèi)容。

上述基本對(duì) Module 的本質(zhì)及其復(fù)用機(jī)制有一定的了解,是不是無(wú)腦開(kāi)啟 Moudle 就可以了呢?

其實(shí)不然!

我們?cè)趯?shí)踐中發(fā)現(xiàn)(以基于 cocoapods 管理為例)在 fmodules-cache-path 的路徑下存在很多份的 pcm 緩存文件,針對(duì)同一個(gè)工程就會(huì)發(fā)現(xiàn)存在多個(gè)下面的現(xiàn)象:

可以發(fā)現(xiàn)在工程的一次編譯下,會(huì)出現(xiàn)多個(gè)目錄出現(xiàn)同一個(gè) module 的緩存情況(eg:lifeCirclePod-1EBT2E5N8K8FN.pcm)。之前講過(guò) Module 機(jī)制是一次編譯后續(xù)復(fù)用的嗎?實(shí)際情況好像與我們的理論沖突!這就要求我們?nèi)ド钊胩骄?Module 復(fù)用的機(jī)制。

追尋 Clang 的源碼發(fā)現(xiàn)編譯器進(jìn)行預(yù)處理器 Preprocessor 的創(chuàng)建時(shí),會(huì)根據(jù)自身工程的參數(shù)來(lái)設(shè)定 Module 緩存的路徑。

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

在實(shí)際的工程中,常常不同 pod 間的 build settting 不同,導(dǎo)致在編譯過(guò)程中會(huì)生成不同的 hash 目錄,從而緩存查找時(shí)候會(huì)出現(xiàn)查找不到 pcm 緩存而重復(fù)生成 Module 緩存的現(xiàn)象。這也解釋了我們上面發(fā)現(xiàn)不同的緩存 hash 目錄下會(huì)出現(xiàn)相同名字的 pcm 緩存。了解 Module 緩存的因素可以有助于在復(fù)雜的工程場(chǎng)景中,提高 Module 的復(fù)用率減少 Module Complier 的時(shí)間。

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

4.3 PCM

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

提到 PCM 文件,我們第一時(shí)間很容易聯(lián)想到 PCH。PCH 文件的應(yīng)用大家應(yīng)該都很熟悉,根據(jù)蘋(píng)果在介紹 PCH 的官方文檔中結(jié)構(gòu)如下:

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

Module 的編譯是在獨(dú)立的線程,獨(dú)立的編譯實(shí)體過(guò)程,與我們輸出目標(biāo)文件對(duì)應(yīng)的前端 action 不同,它所對(duì)應(yīng)的FrontAction為GenerateModuleAction。Module 的機(jī)制思想主要是提供一種語(yǔ)義化的模塊導(dǎo)入方式。所以 PCM 的緩存內(nèi)容同樣會(huì)經(jīng)過(guò)詞法,語(yǔ)法,語(yǔ)義分析的過(guò)程,PCM 文件中的 AST 模塊的序列化保存是在發(fā)現(xiàn)在語(yǔ)義分析之后。

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

其中 Types,Declarations 等信息的寫(xiě)入流程發(fā)生在 ASTBlock 階段。由于在處理處理 ModuleMap 文件的編譯流程中會(huì)對(duì) umbrella.h 中所暴露的頭文件進(jìn)行預(yù)處理,詞法,語(yǔ)法,語(yǔ)義分析等流程。我們?cè)谑褂?WriteAST 寫(xiě)入時(shí),會(huì)將當(dāng)前編譯實(shí)體的 Sema 類(該類是 build AST 和語(yǔ)義分析的實(shí)現(xiàn)類)傳遞過(guò)來(lái)。Sema 持有當(dāng)前的 ASTContext,ASTContext 則可以用于訪問(wèn)當(dāng)前抽象語(yǔ)法樹(shù)上的所有 Nodes(例如 types,decls)等信息。

如果所示:ASTWriter 將已經(jīng)解析無(wú)誤的 Module 信息,包括 AST 等內(nèi)容寫(xiě)入 Module 的緩存文件 PCM 中。

我們?cè)谠创a跟蹤過(guò)程中可以發(fā)現(xiàn)會(huì)將AST節(jié)點(diǎn)信息等寫(xiě)入PCM中的ASTBlock中,我們可以通過(guò)打印獲取到節(jié)點(diǎn)的類型和節(jié)點(diǎn)的名稱:

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

ModuleMap 文件用來(lái)描述從頭文件到模塊邏輯結(jié)構(gòu)的映射關(guān)系,Umbrella 或者Umbrella Header 描述了子Module的概念;

Module 的構(gòu)建是“獨(dú)立”進(jìn)行的,Module 間存在依賴時(shí),優(yōu)先編譯完成被依賴的Module;

Clang 提供了 Module 的新用法(@import ModuleName),但是針對(duì)就項(xiàng)目無(wú)需改造,Clang 在預(yù)處理時(shí)期提供了 Module 與非 Module 的轉(zhuǎn)換;

Module 提供了復(fù)用的機(jī)制,它將暴露外界的 API 以 ASTFile 格式存儲(chǔ),在代碼未發(fā)生變化時(shí),直接讀取緩存。而在代碼變動(dòng)時(shí),Xcode 會(huì)在合適的時(shí)機(jī)對(duì) Module 進(jìn)行更新,開(kāi)發(fā)者無(wú)需額外干預(yù)。

同城編譯時(shí)間數(shù)據(jù)分析

鑒于在58同城工程上實(shí)施的編譯數(shù)據(jù)時(shí)間的加長(zhǎng)的背景,我們?cè)谏钊胩骄?Module 構(gòu)建,復(fù)用等機(jī)制后,我們針對(duì)整個(gè)編譯流程做了詳細(xì)的編譯階段的插樁。

5.1 分析工具

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

編譯完成后clang會(huì)在編譯目錄下,為每個(gè)源文件自動(dòng)生成一個(gè)json文件,文件名和源碼文件相同。

每個(gè)json文件中大概會(huì)有ExecuteCompiler、Frontend、Source、Module Load、Backend等打點(diǎn)數(shù)據(jù),也有Total ExecuteCompiler、Total Frontend、Total Source、Total Module Load、Total Backend這樣的數(shù)據(jù),后者是前者的一個(gè)匯總,這是clang自帶的,也可以在clang中去擴(kuò)展。通過(guò)chrome://tracing/可以很方便查看單個(gè)json文件的耗時(shí)分布,如下。

-ftime-trace設(shè)置后主要時(shí)間段說(shuō)明:

Total ExecuteCompiler:文件編譯總時(shí)間;

Total Frontend:前端編譯時(shí)間,如在clang中編譯時(shí)間;

Total Source:頭文件處理時(shí)間,如處理import;

Total Module Load:Module的加載時(shí)間,如在Source的處理過(guò)程中,判斷當(dāng)前import的是一個(gè)module,則會(huì)執(zhí)行此操作,如import系統(tǒng)庫(kù);

Total Module Compile:Module的編譯時(shí)間,如第一次加載自定義的源碼Module,會(huì)對(duì)Module進(jìn)行編譯,生成AST緩存起來(lái);

Total Backend:編譯器后端處理時(shí)間。

這些時(shí)間段都是Clang中已有的打點(diǎn),從前面的chrome://tracing/圖也能看出來(lái)是有一些包含關(guān)系的,如:

    ExecuteCompiler 包含F(xiàn)rontend和Backend;

    Frontend包含Source;

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

    Module Load包含Module Compile。

5.2 時(shí)間段分析

先選取單個(gè)文件進(jìn)行分析,將其拖到chrome://tracing/中,可看到如下數(shù)據(jù)。

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

上面是從單個(gè)文件進(jìn)行分析,并不能代表整體項(xiàng)目的編譯情況,因此,我們做了一個(gè)自動(dòng)化工具,將所有.json文件中的對(duì)應(yīng)時(shí)間進(jìn)行統(tǒng)計(jì)匯總,得出整體各個(gè)時(shí)間段的匯總數(shù)據(jù),如下。說(shuō)明一下,我們統(tǒng)計(jì)的Total ExecuteCompiler指每個(gè)文件的編譯時(shí)間總和,相當(dāng)于在單核下編譯時(shí)間,而前面顯示的實(shí)際整體的編譯時(shí)間少很多,是因?yàn)槲覀儗?shí)際是在多核下編譯。

從整體分析圖上可看出,Total Frontend時(shí)間均占總編譯時(shí)間Total ExecuteCompiler的80%以上,而Total Frontend中時(shí)間Total Source的總時(shí)間占80%以上,而在Total Source中Total Module Load時(shí)間占70%左右。總時(shí)間Total ExecuteCompiler和前端Total Frontend依然是module下更長(zhǎng),而在Total Frontend中Total Module Load的時(shí)長(zhǎng)在module下明顯比非module下長(zhǎng)很多,跟上面單文件分析的結(jié)論基本一致。這里需要注意的是,Total ExecuteCompiler時(shí)間比前面統(tǒng)計(jì)的總時(shí)間長(zhǎng)很多,是因?yàn)轫?xiàng)目是在多核下編譯,而Total ExecuteCompiler統(tǒng)計(jì)的是所有文件編譯時(shí)間總和,而前面統(tǒng)計(jì)的時(shí)間是多文件并行編譯下的時(shí)間,其它各段時(shí)間同理。

在Total Module Load中會(huì)執(zhí)行Module的編譯,但從上圖我們可以看到其實(shí)Total Module Compile時(shí)間很短,都不超過(guò)50S,因此還需要進(jìn)一步分析Total Module Load的耗時(shí)操作。為此我們根據(jù)clang中的處理流程,在clang中Module Load處理代碼中擴(kuò)展兩個(gè)打點(diǎn):

Module ReadAST:驗(yàn)證Module緩存并反序列化Module cache PCM文件的時(shí)長(zhǎng);

Module WaitForLock:一個(gè)線程在ModuleCompiler期間,其他線程需要掛起等待的時(shí)長(zhǎng)。

并在頭文件查找擴(kuò)展打點(diǎn):

Lookup HeaderFile :預(yù)處理階段查找導(dǎo)入頭文件的磁盤(pán)路徑時(shí)間。

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

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

因此,我們可以得出,相比非module,module化編譯更為耗時(shí),而主要耗時(shí)在驗(yàn)證Module緩存并反序列化操作。那么問(wèn)題來(lái)了,有什么辦法可以在module開(kāi)啟的情況下進(jìn)行編譯時(shí)間優(yōu)化呢?

編譯時(shí)間的優(yōu)化

從上面的數(shù)據(jù)分析我們知道,如果底層組件進(jìn)行 Module 化,并且上層組件通過(guò)module方式進(jìn)行引用的話,會(huì)更耗時(shí)。但是為了支持 Swift/OC 混編,如 Swift 調(diào)用 OC,需要對(duì)組件進(jìn)行 Module 化。因此,我們需要在 Module 化的基礎(chǔ)上優(yōu)化編譯時(shí)間,如果上層組件不通過(guò) Module 方式調(diào)用其它 Module 化的組件,而采用非 Module 化方式進(jìn)行引用,理論上是能避免上述module化操作的耗時(shí)。

6.1 優(yōu)化方案

為了進(jìn)一步優(yōu)化混編下的編譯時(shí)間,我們參考蘋(píng)果 WWDC 2018 的 header search path 中 headermap 查找方案,主要思路是通過(guò) hmap 的方式來(lái)替換header search path 下的文件搜索,來(lái)減少編譯耗時(shí),為描述方便,我們稱為hmap方案,目前業(yè)內(nèi)美團(tuán)對(duì) hmap 有應(yīng)用,并且有 50% 的優(yōu)化效果。58同城也對(duì) headermap 方案進(jìn)行了研究并進(jìn)行了落地,理想的實(shí)現(xiàn)方案就是做一個(gè) cocoapods 插件,在插件中做了以下幾件事:

    HooksManager注冊(cè)cocoapods的post_install鉤子;

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

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

    修改各pod的USE_HEADERMAP值,關(guān)閉對(duì)默認(rèn)的hmap文件的訪問(wèn)。

58對(duì)應(yīng)的插件名為cocoapods-wbhmap,插件完成后,在Podfile中通過(guò)plugin 'cocoapods-wbhmap'接入。

6.2 優(yōu)化數(shù)據(jù)

以下是58同城分別在非 Module、Module 化和優(yōu)化后的 hmap 三種場(chǎng)景下編譯時(shí)間數(shù)據(jù),這里的 hmap 是在各組件 Module 化的基礎(chǔ)上使用的。

首先說(shuō)明一下,這里的整體編譯時(shí)間數(shù)據(jù)上跟前面不一致,是因?yàn)橹匦戮幾g了,每次編譯時(shí)間略有不同,但不影響我們分析。從整體時(shí)間來(lái)看 Module 下的編譯時(shí)間比非 Module 下略長(zhǎng),而 hmap 比非 Module 下優(yōu)化了 32% 左右,比 Module 下優(yōu)化了 33% 左右,可以看出 hmap 的優(yōu)化效果是很顯著的。

接下來(lái)分析一下編譯各階段的時(shí)間,是不跟我們預(yù)想的一致,我們預(yù)想的是 Total Lookup HeaderFile 和 hmap 在 Module Load 階段加載的 Module基本是系統(tǒng)庫(kù),應(yīng)當(dāng)時(shí)間上差不多,而由于hmap節(jié)省了在眾多目錄下文件搜索的時(shí)間,應(yīng)當(dāng)在Total Lookup HeaderFile有較大差別。

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

6.3 hmap 優(yōu)化了什么?

針對(duì) ReadAST 階段再次細(xì)分打點(diǎn)計(jì)時(shí),發(fā)現(xiàn)在 ReadAST 階段去讀取緩存時(shí)候,會(huì)對(duì)緩存 PCM 文件的 ControlBlock 塊信息進(jìn)行解析,該內(nèi)容包含了當(dāng)前 Module 緩存引用外界其他 ASTFile 的記錄。而加載外界 ASTFile 的 PCM 緩存時(shí)候,會(huì)針對(duì)該 ModuleName 進(jìn)行驗(yàn)證確保我們不會(huì)加載一個(gè) non-Module 的 ASTFile 作為一個(gè) Module。它通過(guò)查詢是否存在 ModuleMap 文件來(lái)描述 Module 對(duì)應(yīng)當(dāng)前要查詢的 ModuleName。

我們將重點(diǎn)聚焦在這個(gè)階段,因?yàn)槲覀?hmap 方案最直接的優(yōu)化之處在減少了 Header Search Path 的參數(shù)路徑,將預(yù)處理期間的頭文件查找轉(zhuǎn)換為 key-value 查找,從而減少了在 Header Search Path 眾多 pod 的目錄中(如private、public)的搜索時(shí)間,源碼中 SearchDirs 即為這些目錄,Header Search Path 中目錄越多,SearchDirs 中元素更多,要遍歷的目錄就更多,無(wú)用的搜索時(shí)間就越長(zhǎng),通過(guò)單個(gè)文件進(jìn)行調(diào)試發(fā)現(xiàn)這里消耗的時(shí)間約有 70%,而系統(tǒng)庫(kù)的查找在這里耗時(shí)較長(zhǎng),因?yàn)榘凑站幾g器搜索的順序,系統(tǒng)庫(kù)目錄的是排在 Header Search Path 后的,經(jīng)過(guò)一頓徒勞的搜索之后才到系統(tǒng)庫(kù)目錄搜索,效率較低。

我們猜想前面非 Module 和 hmap 在 Module Load 時(shí)間差較大的原因應(yīng)當(dāng)就在此,因此在 ReadAST 階段的 HeaderSearch::lookupModule 方法內(nèi)打個(gè)點(diǎn) Lookup Module,即 Module ReadAST 包含 Lookup Module,重新編譯進(jìn)行數(shù)據(jù)統(tǒng)計(jì)如下:

這里只統(tǒng)計(jì)非 Module 和 hmap,整體編譯時(shí)間如下:

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

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

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

上面數(shù)據(jù)也進(jìn)一步驗(yàn)證了我們對(duì)于 hmap 編譯時(shí)間優(yōu)化原因的猜想。到這里我們就從數(shù)據(jù)和原理上對(duì) hmap 方案的編譯優(yōu)化做了一個(gè)完整的分析。

總結(jié)

由于 Swift/OC 混編項(xiàng)目的需要,58同城對(duì)組件進(jìn)行了 Module 化,并且嘗試讓所有組件通過(guò) Module 方式進(jìn)行頭文件引用。但我們發(fā)現(xiàn)編譯時(shí)間卻比非 Module 情況下更長(zhǎng),這也與蘋(píng)果官方在 WWDC2013 中的 Module 性能分析結(jié)果不符。

然后在尋求編譯時(shí)間的優(yōu)化方案時(shí),發(fā)現(xiàn)在 WWDC2018 中有提到 hmap 機(jī)制,并借鑒業(yè)內(nèi)的一些寶貴經(jīng)驗(yàn),采用了 hmap 方案對(duì)編譯時(shí)間進(jìn)行優(yōu)化。Module 方案雖無(wú)法降低編譯耗時(shí),但對(duì)比之前混編的橋接方式,可增強(qiáng)項(xiàng)目向 Swift 遷移過(guò)程中混編組件的可維護(hù)性。通過(guò) hmap 方案對(duì)編譯時(shí)間進(jìn)行優(yōu)化,同城最終編譯時(shí)間比 Module 化之前優(yōu)化了約 35%,對(duì)于其它 App 的 Module 化也是有較好的借鑒意義。

作者簡(jiǎn)介

趙志:58同城-用戶價(jià)值增長(zhǎng)部

曾慶?。?8同城-用戶價(jià)值增長(zhǎng)部

顧夢(mèng)奇:58同城-房產(chǎn)事業(yè)群

王強(qiáng):58同城-招聘客戶端

趙發(fā):58同城-汽車(chē)事業(yè)群

參考文獻(xiàn)

LLVM源碼:github/llvm/llvm-project

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

蘋(píng)果WWDC 2013 Advances in Objective-C Module相關(guān)視頻:developer.apple/videos/play/wwdc2013/404/

蘋(píng)果WWDC 2018 Header Search Path相關(guān)視頻:developer.apple/videos/play/wwdc2018/415/

LLVM開(kāi)發(fā)者大會(huì)Doug Gregor的視頻和PPT:llvm.org/devmtg/2012-11/

ftime-trace耗時(shí)報(bào)告配置:blog.csdn/wwchao2012/article/details/109147192

美團(tuán)編譯速度優(yōu)化公眾號(hào)文章: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結(jié)構(gòu):clang.llvm.org/docs/PCHInternals.html#pchinternals-modules

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

 
(文/企資小編)
打賞
免責(zé)聲明
本文為企資小編推薦作品?作者: 企資小編。歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明原文出處:http://biorelated.com/news/show-172440.html 。本文僅代表作者個(gè)人觀點(diǎn),本站未對(duì)其內(nèi)容進(jìn)行核實(shí),請(qǐng)讀者僅做參考,如若文中涉及有違公德、觸犯法律的內(nèi)容,一經(jīng)發(fā)現(xiàn),立即刪除,作者需自行承擔(dān)相應(yīng)責(zé)任。涉及到版權(quán)或其他問(wèn)題,請(qǐng)及時(shí)聯(lián)系我們郵件:weilaitui@qq.com。
 

Copyright ? 2016 - 2023 - 企資網(wǎng) 48903.COM All Rights Reserved 粵公網(wǎng)安備 44030702000589號(hào)

粵ICP備16078936號(hào)

微信

關(guān)注
微信

微信二維碼

WAP二維碼

客服

聯(lián)系
客服

聯(lián)系客服:

在線QQ: 303377504

客服電話: 020-82301567

E_mail郵箱: weilaitui@qq.com

微信公眾號(hào): weishitui

客服001 客服002 客服003

工作時(shí)間:

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

反饋

用戶
反饋