Clang Module 是大概 2013 年左右出現(xiàn)的揭芍,它的出現(xiàn)是為了解決傳統(tǒng)基于 C 語(yǔ)言的編程語(yǔ)言的頭文件包含的弊端馒索。也是現(xiàn)代 Apple 平臺(tái)軟件開發(fā)一定會(huì)用到的一個(gè)技術(shù)萍悴,了解 Clang Module 對(duì)我們組織代碼結(jié)構(gòu)翔始,理解 Xcode 編譯流程彪置,優(yōu)化編譯速度斩箫,定位編譯錯(cuò)誤等都會(huì)有幫助吏砂。
傳統(tǒng)頭文件包含的弊端
傳統(tǒng)的頭文件包含,存在以下幾個(gè)主要的問(wèn)題:
編譯性能問(wèn)題
對(duì)于傳統(tǒng)頭文件包含乘客,預(yù)處理器會(huì)將頭文件的內(nèi)容復(fù)制粘貼過(guò)來(lái)替換 #include 預(yù)處理指令狐血。而很多頭文件內(nèi)都會(huì)包含相同的其它頭文件,例如底層依賴易核,系統(tǒng)庫(kù)等匈织,這樣造成了不同的源文件中會(huì)出現(xiàn)重復(fù)的內(nèi)容。也就是說(shuō)牡直,對(duì)于 M 個(gè)源文件缀匕,如果有 N 個(gè)頭文件,復(fù)雜度量級(jí)是 M x N 的碰逸,編譯器會(huì)對(duì)每個(gè)重復(fù)的內(nèi)容都進(jìn)行一次文本分析乡小,做了大量重復(fù)的工作,拖慢了編譯時(shí)間饵史。
例如系統(tǒng)的 Foundation 框架满钟,框架內(nèi)嵌套包含了 800 個(gè)以上的其它頭文件胜榔,整個(gè)框架的大小在 9MB 以上,作為最最基礎(chǔ)的框架湃番,幾乎每個(gè)源文件都會(huì)包含 Foundation.h夭织。對(duì)于傳統(tǒng)的頭文件包含,F(xiàn)oundation.h 的內(nèi)容及其包含的其它頭文件會(huì)被不斷地重復(fù)進(jìn)行詞法分析語(yǔ)義分析吠撮,拖慢編譯速度尊惰。
脆弱性
脆弱性是因?yàn)?#include 替換得到的內(nèi)容會(huì)受到其它預(yù)處理指令的影響。例如頭文件中有某個(gè)符號(hào) XXX泥兰,如果在包含這個(gè)頭文件之前弄屡,存在類似 #define XXX "other text"
這樣的宏定義,就會(huì)導(dǎo)致頭文件中的所有 XXX 都被替換為 "other text"逾条,而導(dǎo)致編譯錯(cuò)誤琢岩。
#define XXX "other text"
#include "XXX.h"
必須使用慣例方案來(lái)解決一些問(wèn)題
傳統(tǒng)的頭文件包含無(wú)法解決頭文件重復(fù)包含的問(wèn)題,因此大家都用一種慣例來(lái)避免重復(fù)包含师脂。
#ifndef __XXXX_H__
#define __XXXX_H__
// 頭文件內(nèi)容
#endif
雖然現(xiàn)代開發(fā)工具都能自動(dòng)生成這些担孔,但還是存在一定的不便。
另外為了解決宏在多個(gè)庫(kù)之間重名的問(wèn)題吃警,大家都會(huì)把宏的名稱起的很長(zhǎng)很長(zhǎng)糕篇,增加前綴和后綴。
對(duì)工具的迷惑性
在 C 語(yǔ)言為基礎(chǔ)的語(yǔ)言中,軟件庫(kù)的邊界不是很清晰,比如很難辨別一個(gè)頭文件到底是哪個(gè)語(yǔ)言的镣煮,因?yàn)?C、C++墩崩、Objective-C 等語(yǔ)言的頭文件都是 .h。也很難弄清楚一個(gè)頭文件到底是屬于哪個(gè)庫(kù)的侯勉。這對(duì)于開發(fā)基于這些軟件庫(kù)的工具帶來(lái)了一定的難度鹦筹。
Clang Module 能解決什么問(wèn)題?
語(yǔ)義導(dǎo)入
Clang Module 從傳統(tǒng)頭文件包含的文本導(dǎo)入改進(jìn)成了更健壯址貌,效率更高的語(yǔ)義導(dǎo)入铐拐。當(dāng)編譯器看到一個(gè) Module 導(dǎo)入指令時(shí),編譯器會(huì)去加載一個(gè)二進(jìn)制文件练对,這個(gè)二進(jìn)制文件提供了這個(gè)模塊所有 API 的信息遍蟋,這些 API 可以直接給其它代碼使用。
編譯性能提升
Clang Module 提升了編譯性能螟凭,每個(gè)模塊只需要編譯一次虚青,然后會(huì)生成一個(gè)模塊的二進(jìn)制表示(.pcm,預(yù)編譯模塊螺男,下文會(huì)說(shuō)明)挟憔,并緩存到磁盤上钟些。下次遇到 import 這個(gè)模塊時(shí)烟号,編譯器不需要再次編譯 Module绊谭,而是直接讀取這個(gè)緩存的二進(jìn)制表示即可。
上下文無(wú)關(guān)
Clang Module 解決了脆弱性的問(wèn)題汪拥,每個(gè) Module 都是一個(gè)獨(dú)立的實(shí)體达传,會(huì)被隔離地、獨(dú)立的編譯迫筑,是上下文無(wú)關(guān)的宪赶。當(dāng) import 模塊時(shí),會(huì)忽略 import 上下文的其它的預(yù)處理指令脯燃,這樣 import 之前的預(yù)處理指令不會(huì)對(duì)模塊導(dǎo)入產(chǎn)生任何影響搂妻。
每個(gè)模塊都是一個(gè)自包含的個(gè)體,他們上下文無(wú)關(guān)辕棚,相互隔離欲主,因此不在需要使用一些慣例方法來(lái)避免出現(xiàn)一些問(wèn)題,因?yàn)檫@些問(wèn)題已經(jīng)不會(huì)出現(xiàn)了逝嚎。
自己制作一個(gè)模塊
為了能對(duì)有一個(gè)直觀的了解扁瓢,我們可以自己動(dòng)手制作一個(gè) 模塊。用 Xcode 創(chuàng)建一個(gè)新的 iOS app 工程作為測(cè)試使用补君。然后在工程根目錄下新建一個(gè) group 命名為 Frameworks引几。
在命令行中進(jìn)入 Frameworks 文件夾,新建一個(gè) Dog.framework 文件夾挽铁,名字可以隨意伟桅,這里是隨便起的。
mkdir Dog.framework
然后回到 Xcode 中叽掘,在 Frameworks 目錄上右擊鼠標(biāo)楣铁,選擇 Adds files to ... 把 Dog.framework 添加到 Frameworks 目錄內(nèi)。
此時(shí)編譯會(huì)報(bào)錯(cuò) Framework not found Dog
够掠,接下來(lái)我們看看怎樣制作出一個(gè) Xcode 能正確識(shí)別并編譯的模塊民褂。
在 Dog.framework 中新建 Dog.swift 文件并添加以下內(nèi)容:
// Dog.swift
import Foundation
public class Dog: NSObject {
public func bark() {
print("bark")
}
@objc func objcBark() {
print("objc bark")
}
}
接下來(lái)我們來(lái)為這個(gè) framework 生成接口文件。在命令行中執(zhí)行以下命令:
swiftc -module-name Dog -c Dog.swift -target arm64-apple-ios16.2-simulator -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.2.sdk -emit-module -emit-objc-header -emit-objc-header-path Dog-Swift.h
swiftc 是 Swift 語(yǔ)言的編譯器疯潭,它底層也調(diào)用了 clang赊堪。下面對(duì)參數(shù)一一進(jìn)行說(shuō)明:
- -module-name Dog 模塊的名稱,使用者可以通過(guò) import + 這個(gè)名稱來(lái)引入模塊竖哩。
- -c Dog.swift 指定要編譯的源文件哭廉。
- -target arm64-apple-ios16.2-simulator 指定生成目標(biāo)的架構(gòu)。
- -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.2.sdk 指定要鏈接進(jìn)來(lái)的 SDK相叁,這里使用的是 iOS 16.2 的模擬器版本遵绰。
- -emit-module 會(huì)生成一個(gè) .swiftdoc 文件和一個(gè) .swiftmodule 文件辽幌。
- -emit-objc-header 生成 Objective-C 頭文件,僅包含被標(biāo)記為 @objc 的符號(hào)椿访。
- -emit-objc-header-path 指定 Objective-C 頭文件的路徑乌企,這里我們遵循了 Xcode 的慣例,使用 ”模塊名+Swift.h“ 來(lái)命名成玫。
雖然需要的文件已經(jīng)生成了加酵,但是并不是 Xcode 支持的 module 目錄結(jié)構(gòu),無(wú)法被 Xcode 讀取哭当。我們可以通過(guò)觀察 Xcode 創(chuàng)建的 Framework 來(lái)了解這種結(jié)構(gòu)猪腕,來(lái)創(chuàng)建正確的結(jié)構(gòu)。
在 Dog.framework 文件夾中創(chuàng)建 Headers 文件夾钦勘,然后把 Dog-Swift.h 移動(dòng)到 Headers 文件夾中陋葡。然后在 Dog.framework 文件夾中再創(chuàng)建一個(gè) Modules 文件夾,然后在 Modules 文件夾中創(chuàng)建 Dog.swiftmodule 文件夾彻采,把 Dog.swiftdoc 和 Dog.swiftmodule 移動(dòng)到 Dog.swiftmodule 文件夾中腐缤。最后把這兩個(gè)文件重命名為 arm64.swiftdoc 和 arm64.swiftmodule。
當(dāng)前 Dog.framework 的目錄結(jié)構(gòu)為:
Dog.framework/
|---- Dog
|---- Headers
| |---- Dog-Swift.h
|---- Modules
|---- Dog.swiftmodule
|---- arm64.swiftdoc
|---- arm64.swiftmodule
現(xiàn)在接口已經(jīng)有了颊亮,但是還沒有二進(jìn)制庫(kù)文件柴梆,依然無(wú)法編譯通過(guò),下面我們來(lái)生成二進(jìn)制庫(kù)文件终惑。
執(zhí)行以下命令:
swiftc -module-name Dog -parse-as-library -c Dog.swift -target arm64-apple-ios16.2-simulator -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.2.sdk -emit-object
這個(gè)命令中有很多參數(shù)跟上一個(gè)命令是一樣的绍在,在此不做重復(fù)說(shuō)明,僅說(shuō)明下上個(gè)命令中沒有的參數(shù)雹有。
- -parse-as-library 讓編譯器把文件解釋為一個(gè)庫(kù)偿渡,而不是一個(gè)可執(zhí)行文件。
- -emit-object 輸出目標(biāo)文件霸奕。
這個(gè)命令執(zhí)行完以后會(huì)生成 Dog.o 這個(gè)目標(biāo)文件溜宽,然后我們需要把目標(biāo)文件歸檔為庫(kù)。
libtool -static Dog.o -arch_only arm64 -o Dog
這里為了簡(jiǎn)化流程選擇了創(chuàng)建靜態(tài)庫(kù)质帅,而不是動(dòng)態(tài)鏈接庫(kù)适揉,因此使用 -static。這時(shí)在 Dog.framework 中會(huì)出現(xiàn) Dog 這個(gè)二進(jìn)制靜態(tài)庫(kù)文件煤惩。此時(shí)我們?cè)?ViewController 中 import Dog 然后編譯工程嫉嘀,就能編譯通過(guò)了。說(shuō)明當(dāng)前這種目錄結(jié)構(gòu)可以讓 Xcode 正確找到模塊所需文件魄揉。
Module map
接下來(lái)我們來(lái)試一試用 Objective-C 來(lái)調(diào)用 Dog 模塊會(huì)怎樣剪侮。
在上面的工程中再創(chuàng)建一個(gè) Objective-C 類,命名為 OCObject洛退,并讓 Xcode 自動(dòng)創(chuàng)建頭文件橋接文件瓣俯,并添加以下代碼:
// OCObject.h
@interface OCObject : NSObject
- (void)doSomething;
@end
// OCObject.m
#import "OCObject.h"
#import <Dog/Dog-Swift.h>
@implementation OCObject
- (void)doSomething {
Dog *dog = [[Dog alloc] init];
[dog objcBark];
}
@end
會(huì)發(fā)現(xiàn)此時(shí)是可以打印出 objc mark
的杰标。然后把 #import <Dog/Dog-Swift.h>
替換成標(biāo)準(zhǔn)的模塊導(dǎo)入語(yǔ)法 @import Dog;
,編譯卻報(bào)錯(cuò)了彩匕,提示 ”Module 'Dog' not found“腔剂。
這時(shí)因?yàn)?framework 中缺少了一個(gè)重要的 modulemap 文件,Xcode 就無(wú)法找到模塊推掸。#import <Dog/Dog-Swift.h>
之所以有效是因?yàn)樗旧硎且粋€(gè)向前兼容的語(yǔ)句桶蝎,如果 framework 支持模塊,則導(dǎo)入模塊谅畅,如果 framework 不支持模塊,它會(huì)像 #include
一樣去搜索路徑中找到這個(gè)頭文件噪服,直接把文本內(nèi)容粘貼到這里毡泻。
Module map 指明了 framework 中的頭文件邏輯結(jié)構(gòu)應(yīng)該如何映射為模塊。參考用 Xcode 創(chuàng)建 framework 時(shí)自動(dòng)創(chuàng)建的 module map 文件粘优,會(huì)發(fā)現(xiàn)在 Modules 文件夾下有一個(gè) module.modulemap 文件仇味,其內(nèi)容如下:
framework module ObserveModuleStructure {
umbrella header "ObserveModuleStructure.h"
export *
module * { export * }
}
module ObserveModuleStructure.Swift {
header "ObserveModuleStructure-Swift.h"
requires objc
}
通過(guò)參考 clang 的文檔,來(lái)對(duì)這個(gè)語(yǔ)法一一進(jìn)行說(shuō)明:
-
framework module XXXX
定義了一個(gè) framework 語(yǔ)義的模塊 -
umbrella header "XXXX.h"
說(shuō)明把 XXXX.h 文件作為模塊的 unbrella header雹顺,傘頭文件相當(dāng)于模塊中所有公共頭文件的一個(gè)集合丹墨,方便使用者導(dǎo)入。 -
export *
將所有子模塊中的符號(hào)進(jìn)行重導(dǎo)出到主模塊中 -
module * { export * }
定義子模塊嬉愧,這里為 * 則是為 umbrella header 中的每個(gè)頭文件都創(chuàng)建一個(gè)子模塊贩挣。
根據(jù)這個(gè)語(yǔ)法編寫自己的 module map 文件,路徑為 Dog.framework/Modules/module.modulemap:
// Dog.framework/Modules/module.modulemap
framework module Dog {
umbrella header "Dog.h"
export *
module * { export * }
}
module Dog.Swift {
header "Dog-Swift.h"
requires objc
}
此時(shí)依然編譯報(bào)錯(cuò)没酣,還需要一個(gè) unbrella header 文件王财,創(chuàng)建一個(gè) Dog.h 文件放到 Dog.framework/Headers/ 中,內(nèi)容為空即可裕便。然后就可以編譯通過(guò)绒净,打印出 bark objc。
Module Map 語(yǔ)言語(yǔ)法
官方把這種語(yǔ)法叫做模塊映射語(yǔ)言(Module Map Language)偿衰。
根據(jù) Clang 的文檔挂疆,模塊映射語(yǔ)言在 Clang 的大版本之間可能不會(huì)保持穩(wěn)定,因此在平常的開發(fā)中下翎,讓 Xcode 去自動(dòng)生成就好缤言。
模塊聲明
[framework] module module-id [extern_c] [system] {
module-member
}
framework
framework 代表這個(gè)模塊是是一個(gè) Darwin 風(fēng)格的 framework。Darwin 風(fēng)格的 framework 主要出現(xiàn)在 macOS 和 iOS 操作系統(tǒng)中漏设,它的全部?jī)?nèi)容都包含在一個(gè) Name.framework 文件夾中墨闲,這個(gè) Name 就是 framework 的名字,這個(gè)文件夾的內(nèi)容布局如下:
Name.framework/
Modules/module.modulemap framework 的模塊映射
Headers/ 包含了 framework 中的頭文件
PrivateHeaders/ 包含了 framework 中私有的頭文件
Frameworks/ 包含嵌入的其它 framework
Resources/ 包含額外的資源
Name 指向共享庫(kù)的符號(hào)鏈接
system
system 指定了這個(gè)模塊是一個(gè)系統(tǒng)模塊郑口。當(dāng)一個(gè)系統(tǒng)模塊被重編譯后鸳碧,模塊的所有頭文件都會(huì)被當(dāng)做系統(tǒng)頭文件盾鳞,這樣一些警告就不會(huì)出現(xiàn)。這和在頭文件中放置 #pragma GCC system_header
等效瞻离。
extern_c
extern_c 指明了模塊中包含的 C 代碼可以被 C++ 使用腾仅。當(dāng)這個(gè)模塊被編譯用來(lái)給 C++ 調(diào)用時(shí),所有模塊中的頭文件都會(huì)被包含在一個(gè)隱含的 extern "C"
代碼塊中套利。
模塊體
模塊體包含了 header推励、requires 等常見的聲明和子模塊聲明,例如:
framework module Dog {
umbrella header "Dog.h"
requires objc
module * { export * }
}
header
header 指定了要把哪些頭文件映射為模塊肉迫。umbrella header 則是指定了綜合性傘頭文件验辞。
requires
requires 聲明指定了導(dǎo)入這個(gè)模塊的編譯單元需要滿足的條件。這個(gè)條件有語(yǔ)言喊衫、平臺(tái)跌造、編譯環(huán)境和目標(biāo)特定功能等。例如 requires cplusplus11
表示模塊需要在支持 C++11 的環(huán)境中使用族购,requires objc
表示模塊需要在支持 Objective-C 語(yǔ)言的環(huán)境中使用壳贪。
module
module 用來(lái)聲明模塊中的子模塊,如果是 module * 則代表模塊中的每個(gè)頭文件都會(huì)作為一個(gè)子模塊寝杖。
子模塊聲明
在主模塊的模塊體中嵌套地聲明模塊就是子模塊违施。例如在 MyLib 模塊中聲明一個(gè)子模塊 A,寫法如下:
module MyLib {
module A {
header "A.h"
export *
}
}
explicit
explicit 修飾符是用來(lái)修飾子模塊的瑟幕。如果想使用被 explicit 修飾的子模塊磕蒲,必須在 import 時(shí)指定子模塊的名字,像這樣 import modulename.submodulename
收苏,或者這個(gè)子模塊已經(jīng)被其它已導(dǎo)入的模塊重導(dǎo)出過(guò)亿卤。
export
export 指定了將哪個(gè)模塊的 API 進(jìn)行重新導(dǎo)出,成為 export 所在的模塊的 API鹿霸。
export_as
export_as 將當(dāng)前模塊的 API 通過(guò)另一個(gè)指定的模塊導(dǎo)出排吴。
module MyFrameworkCore {
export_as MyFramework
}
上面的例子中,MyFrameworkCore 中的 API 將通過(guò) MyFramework 導(dǎo)出懦鼠。
模塊映射語(yǔ)言還包含很多其它的聲明語(yǔ)句钻哩,例如 use
、config_macrs
肛冶、link
街氢、conflict
等,由于在 iOS 開發(fā)中出現(xiàn)的不是很多睦袖,這里就不做一一說(shuō)明珊肃,有興趣可以查看 Clang 的官方文檔。
Clang Module 的緩存機(jī)制
Clang 可以通過(guò)讀取 modulemap 文件的內(nèi)容將 modulemap 中指定的模塊編譯成預(yù)編譯模塊(Precompiled Module),后綴名是 .pcm伦乔。
clang -cc1 -emit-obj use.c -fmodules -fimplicit-module-maps -fmodules-cache-path=prebuilt -fdisable-module-hash
上面的命令通過(guò)指定參數(shù) implicit-module-maps 讓編譯器根據(jù)一定的規(guī)則自己去查找 modulemap 文件厉亏,通過(guò)指定參數(shù) modules-cache-path 告訴編譯器預(yù)編譯模塊的緩存路徑。Clang 會(huì)根據(jù) modulemap 中的信息編譯各個(gè)模塊烈和,將生成的 .pcm 文件放到 prebuilt 目錄下爱只。
.pcm 文件以一種編譯器可以輕松讀取并解析的格式保存了模塊的信息,之后編譯器在編譯其它模塊時(shí)如果遇到了需要依賴這個(gè)模塊招刹,則可以快速的從 .pcm 中讀取模塊信息而不需要重新編譯模塊恬试。
在 Xcode 中使用 Clang Module
用 Xcode 創(chuàng)建的框架或庫(kù)都是默認(rèn)開啟 Clang Module 支持的,也就是在 Build Settings 中疯暑,Defines Module
的設(shè)置為 YES训柴。如果是很老的庫(kù)可能沒有開啟,手動(dòng)把 Defines Module 設(shè)置為 YES 即可缰儿。
當(dāng) Defines Modules 是 YES 是畦粮,Xcode 在編譯工程時(shí)會(huì)給 clang 命令增加 -fmodules 等模塊相關(guān)參數(shù),開啟模塊支持乖阵。
結(jié)語(yǔ)
很多時(shí)候,開發(fā)工具都對(duì)我們隱藏了很多底層的細(xì)節(jié)预麸,了解這些細(xì)節(jié)瞪浸,可以幫助我們了解底層的原理,分析并解決一些棘手的問(wèn)題吏祸。Clang 是 Apple 平臺(tái)上重要的工具对蒲,值得我們?nèi)パ芯刻剿鳌8兄x您的閱讀贡翘,如果文章有不正確的地方蹈矮,或者您有自己的見解,歡迎發(fā)表評(píng)論來(lái)討論鸣驱。