module(模塊):最小的代碼單元
一個(gè)module是機(jī)器代碼和數(shù)據(jù)的最小單位,可以獨(dú)立于其他代碼單位進(jìn)行鏈接。
這句話的的解釋:
通常,module是通過編譯單個(gè)源文件生成的目標(biāo)文件。例如:當(dāng)前的test.m被編譯成目標(biāo)文件test.o時(shí),當(dāng)前的目標(biāo)文件就代表了一個(gè)module。
但是,有一個(gè)問題,Module在調(diào)用的時(shí)候會(huì)產(chǎn)生開銷,比如我們?cè)谑褂靡粋€(gè)靜態(tài)庫(kù)的時(shí)候,可以這樣使用
@import TestStaticFramework;
這個(gè)靜態(tài)庫(kù)中可能包含了許多的.o文件。豈不是要導(dǎo)入很多的Module。并不需要。在靜態(tài)鏈接的時(shí)候,也就是靜態(tài)庫(kù)鏈接到主項(xiàng)目或者動(dòng)態(tài)庫(kù)時(shí),最后生成可執(zhí)行文件或者動(dòng)態(tài)庫(kù)時(shí)。靜態(tài)鏈接器可以把多個(gè)Module鏈接優(yōu)化成一個(gè),來減少本來多個(gè)Module直接調(diào)用的問題。
- 問題一:我們經(jīng)常講.m的編譯,那.h是怎么編譯的?
- 問題二:Module在調(diào)用的時(shí)候會(huì)產(chǎn)生開銷,比如我們?cè)谑褂靡粋€(gè)靜態(tài)庫(kù)的時(shí)候。這種開銷是怎么引起的?系統(tǒng)是怎么優(yōu)化這種開銷?
- 問題三:通常我們?cè)陧?xiàng)目中使用
#import導(dǎo)入AFN,實(shí)際上需要導(dǎo)入很多個(gè)頭文件,這其中產(chǎn)生了什么問題?當(dāng)我們?cè)诖a中導(dǎo)入一個(gè)庫(kù)或者引入其它頭文件的時(shí)候,發(fā)生了什么事情? - 問題四:ModuleMap是什么?
#include與#import差異
案例:現(xiàn)存在3個(gè)頭文件:A.h、B.h、C.h。在B.h中導(dǎo)入頭文件A.h,在C.h中導(dǎo)入頭文件B.h,在C.c中導(dǎo)入C.h。
編譯時(shí),編譯器按順序編譯這些文件,
#include導(dǎo)入方式:
- 先到A時(shí),A中沒有導(dǎo)入其它文件,只需編譯A。
- 到B時(shí),因?yàn)锽中導(dǎo)入了A,A又要編譯一次,需要編譯A和B。
- 到C時(shí),因?yàn)镃導(dǎo)入了B,所以需要編譯B,編譯B時(shí),因?yàn)锽導(dǎo)入了A,A也要再次編譯。即A、B、C都要編譯一次。
#import導(dǎo)入方式:
- 頭文件會(huì)被預(yù)先編譯成二進(jìn)制文件,并且每個(gè)頭文件只會(huì)被編譯一次。此時(shí)無論有多少文件導(dǎo)入頭文件,都不會(huì)被重復(fù)編譯。
驗(yàn)證#include導(dǎo)入方式
可以用指令看看編譯器在預(yù)處理階段幫我們做了哪些事情:
clang -E use.c

無論文件中是#import還是#include ,clang預(yù)編譯出來的結(jié)果是一樣的,意思就是執(zhí)行的流程是一樣的。但是在具體到每個(gè)步驟的時(shí)候,存在差異:#import導(dǎo)入時(shí),直接使用預(yù)先編譯好的二進(jìn)制文件。
每次包含標(biāo)頭時(shí),編譯器都必須可傳遞地預(yù)處理和解析該標(biāo)頭及其包含的每個(gè)標(biāo)頭中的文本。必須對(duì)應(yīng)用程序中的每個(gè)翻譯單元重復(fù)此過程,這涉及大量的冗余工作。
#include偽指令被預(yù)處理程序視為文本包含,因此在包含時(shí)必須接受任何活動(dòng)的宏定義。如果任何活動(dòng)宏定義碰巧與庫(kù)中的名稱沖突,則可能會(huì)破壞庫(kù)API或?qū)е聨?kù)頭本身的編譯失敗。
此外,導(dǎo)入模塊時(shí)將自動(dòng)提供使用該模塊所需的任何鏈接器標(biāo)志
每個(gè)模塊都被解析為一個(gè)獨(dú)立的實(shí)體,因此它具有一致的預(yù)處理器環(huán)境。
此外,在遇到導(dǎo)入聲明時(shí),當(dāng)前的預(yù)處理器定義將被忽略,
@import上面的聲明導(dǎo)入std模塊的全部?jī)?nèi)容(其中將包含例如整個(gè)C或C ++標(biāo)準(zhǔn)庫(kù)),并在當(dāng)前翻譯單元中提供其API。要僅導(dǎo)入模塊的一部分,可以使用點(diǎn)語法來特定特定的子模塊
模塊會(huì)自動(dòng)將#include指令轉(zhuǎn)換為相應(yīng)的模塊導(dǎo)入
關(guān)于開銷的問題:
如果只編譯一個(gè)C.c文件,A、B、C這3個(gè)頭文件都需要編譯一次,兩種導(dǎo)入方式無差異。但是真正的項(xiàng)目中,依賴關(guān)系通常都很復(fù)雜,使用import做到每個(gè)文件只編譯一次,就可以節(jié)省開銷。
在具有N個(gè)翻譯單元和每個(gè)翻譯單元中包含M個(gè)標(biāo)頭的項(xiàng)目中,即使M個(gè)標(biāo)頭中的大多數(shù)在多個(gè)翻譯單元之間共享,編譯器仍在執(zhí)行M x N個(gè)工作。
std.io模塊僅編譯一次,并且將模塊導(dǎo)入轉(zhuǎn)換單元是恒定時(shí)間操作(與模塊系統(tǒng)無關(guān))。因此,每個(gè)軟件庫(kù)的API僅解析一次,從而將M x N編譯問題減少為M + N問題。
用modulemap驗(yàn)證#import導(dǎo)入方式
參考文檔
https://clang.llvm.org/docs/APINotes.html
https://clang.llvm.org/docs/Modules.html#export-declaration
# -fmodules:允許使用module語言來表示頭文件
# -fmodule-map-file:module map的路徑。如不指明默認(rèn)module.modulemap
# -fmodules-cache-path:編譯后的module緩存路徑
clang -fmodules -fmodule-map-file=module.modulemap -fmodules-cache-path=./prebuilt -c use.c -o use.o
新建module.modulemap文件:

module.modulemap用來描述頭文件與module之間映射的關(guān)系
- 定義了名稱為A和B的兩個(gè)module
- 在
module A中,定義了header A.h,表示module A和A.h的映射關(guān)系 - 在
module B中,定義了header B.h,和A同理。export A表示將B.h導(dǎo)入的A.h頭文件重新導(dǎo)出
用clang指令使用fmodules方式生成目標(biāo)文件:

module在Xcode中是默認(rèn)開啟的。如果在Build Settings中,將Enable Modules設(shè)置為NO,導(dǎo)入頭文件將不能使用@import方式。開啟module后,項(xiàng)目中導(dǎo)入頭文件,無論使用#include、#import、@import中何種方式,最終都會(huì)映射為@import方式。
Cocoapod安裝的AFNetworking文件的modulemap
// 聲明framework的module名稱為AFNetworking
framework module AFNetworking {
// 導(dǎo)入文件的集合(如果沒有關(guān)鍵字header那么umbrella后面需要跟上頭文件的文件夾名稱)
umbrella header "AFNetworking-umbrella.h"
export * //把引入的頭文件重新導(dǎo)出。
module * { export * } //把導(dǎo)入頭文件修飾成子module,并把符號(hào)全部導(dǎo)出(第一個(gè)通配符*表示子module名稱和父module名稱一致)
// 如果要指定子module的名稱需要使用explicit關(guān)鍵字
// eg:
explicit module NANetworking {
header "NANetworking.h"
export *
}
}
umbrella:雨傘頭,可以理解為傘柄。一把雨傘的傘柄下有很多傘骨,umbrella的作用是指定一個(gè)目錄,這個(gè)目錄即為傘柄,目錄下所有.h頭文件即為傘骨。
explicit:顯示指明子module名稱。
自定義Module
為什么需要用到自定義Module?
因?yàn)樵谏梢粋€(gè)自定義庫(kù)時(shí),在我們的Framework項(xiàng)目中并沒有幫我們生成ModuleMap文件,它只會(huì)在編譯時(shí)自動(dòng)幫我們生成。這樣就存在一個(gè)問題,如果我們想在項(xiàng)目中配置自己的東西,比如說配置一個(gè)子Module。這種場(chǎng)景下,我們就需要寫一個(gè)自己的Module文件。
- 寫好一個(gè)
Module文件后,將項(xiàng)目加到對(duì)應(yīng)Framework項(xiàng)目的Tartget中。 - 配置設(shè)置中的
Module Map File,路徑的設(shè)置是以SRCRoot為前置路徑的。 - 因?yàn)橄到y(tǒng)默認(rèn)查找
module.modulemap文件。自定義的modulemap文件,無論什么名稱,在編譯后,都會(huì)把文件名稱改成module.modulemap文件名。 -
子module的導(dǎo)出可以用通配符,也可以一個(gè)個(gè)單獨(dú)指定。