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

二維碼
企資網(wǎng)

掃一掃關(guān)注

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

Swift_與_Objective_C_混編

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

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

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

出品 | CSDN(ID:CSDNnews)

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

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

CSDN 付費(fèi)下載自視覺中國

Swift 和 OC 混編開發(fā)

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

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

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

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

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

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

Clang Module 初探

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

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

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

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

2.1 普通 import 的機(jī)制

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

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

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

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

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

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

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

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

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

2.2 PCH (Precompiled Headers)

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

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

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

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

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

2.3 Modules

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

那么 Module 到底是什么呢?

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

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

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

2.4 蘋果對 Module 的解讀

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

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

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

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

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

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

58同城初步實(shí)踐

3.1 Module 化工程配置

組件 Module 化

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

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

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

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

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

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

3.2 Swift/OC 混編橋接文件

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

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

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

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

萬事具備,只差編譯!

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

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

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

Clang Module 原理深究

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

4.1 ModuleMap 與 Umbrella

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

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

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

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

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

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

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

4.2 Module 的構(gòu)建

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

其實(shí)不然!

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

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

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

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

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

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

4.3 PCM

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

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

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

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

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

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

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

我們在源碼跟蹤過程中可以發(fā)現(xiàn)會將AST節(jié)點(diǎn)信息等寫入PCM中的ASTBlock中,我們可以通過打印獲取到節(jié)點(diǎn)的類型和節(jié)點(diǎn)的名稱:

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

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

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

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

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

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

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

5.1 分析工具

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

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

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

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

Total ExecuteCompiler:文件編譯總時間;

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

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

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

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

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

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

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

    Frontend包含Source;

    Source中包含Module Load(前提是如當(dāng)前.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 時間段分析

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6.1 優(yōu)化方案

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

總結(jié)

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

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

作者簡介

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

曾慶?。?8同城-用戶價值增長部

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

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

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

參考文獻(xiàn)

LLVM源碼:github/llvm/llvm-project

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

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

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

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

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

美團(tuán)編譯速度優(yōu)化公眾號文章: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)載請注明原文出處:http://biorelated.com/news/show-172440.html 。本文僅代表作者個人觀點(diǎn),本站未對其內(nèi)容進(jìn)行核實(shí),請讀者僅做參考,如若文中涉及有違公德、觸犯法律的內(nèi)容,一經(jīng)發(fā)現(xiàn),立即刪除,作者需自行承擔(dān)相應(yīng)責(zé)任。涉及到版權(quán)或其他問題,請及時聯(lián)系我們郵件:weilaitui@qq.com。
 

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

粵ICP備16078936號

微信

關(guān)注
微信

微信二維碼

WAP二維碼

客服

聯(lián)系
客服

聯(lián)系客服:

在線QQ: 303377504

客服電話: 020-82301567

E_mail郵箱: weilaitui@qq.com

微信公眾號: weishitui

客服001 客服002 客服003

工作時間:

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

反饋

用戶
反饋