module(模塊):最小的代碼單元
一個module是機器代碼和數(shù)據(jù)的最小單位,可以獨立于其他代碼單位進行鏈接宣增。
這句話的的解釋:
通常,module是通過編譯單個源文件生成的目標文件蚁廓。例如:當前的test.m被編譯成目標文件test.o時盈简,當前的目標文件就代表了一個module。
但是携取,有一個問題攒钳,Module在調用的時候會產(chǎn)生開銷,比如我們在使用一個靜態(tài)庫的時候雷滋,可以這樣使用
@import TestStaticFramework;
這個靜態(tài)庫中可能包含了許多的.o文件不撑。豈不是要導入很多的Module文兢。并不需要。在靜態(tài)鏈接的時候焕檬,也就是靜態(tài)庫鏈接到主項目或者動態(tài)庫時姆坚,最后生成可執(zhí)行文件或者動態(tài)庫時。靜態(tài)鏈接器可以把多個Module鏈接優(yōu)化成一個实愚,來減少本來多個Module直接調用的問題兼呵。
- 問題一:我們經(jīng)常講.m的編譯,那.h是怎么編譯的腊敲?
- 問題二:Module在調用的時候會產(chǎn)生開銷击喂,比如我們在使用一個靜態(tài)庫的時候。這種開銷是怎么引起的碰辅?系統(tǒng)是怎么優(yōu)化這種開銷懂昂?
- 問題三:通常我們在項目中使用
#import
導入AFN,實際上需要導入很多個頭文件没宾,這其中產(chǎn)生了什么問題凌彬?當我們在代碼中導入一個庫
或者引入其它頭文件
的時候,發(fā)生了什么事情循衰? - 問題四:ModuleMap是什么铲敛?
#include
與#import
差異
案例:現(xiàn)存在3個頭文件:A.h
、B.h
会钝、C.h
伐蒋。在B.h
中導入頭文件A.h
,在C.h
中導入頭文件B.h
顽素,在C.c
中導入C.h
咽弦。
編譯時,編譯器按順序編譯這些文件胁出,
#include
導入方式:
- 先到A時型型,A中沒有導入其它文件,只需編譯A全蝶。
- 到B時闹蒜,因為B中導入了A,A又要編譯一次抑淫,需要編譯A和B绷落。
- 到C時,因為C導入了B始苇,所以需要編譯B砌烁,編譯B時,因為B導入了A,A也要再次編譯函喉。即A避归、B、C都要編譯一次管呵。
#import
導入方式:
- 頭文件會被預先編譯成二進制文件梳毙,并且每個頭文件只會被編譯一次。此時無論有多少文件導入頭文件捐下,都不會被重復編譯账锹。
驗證#include
導入方式
可以用指令看看編譯器在預處理階段幫我們做了哪些事情:
clang -E use.c
無論文件中是#import
還是#include
,clang
預編譯出來的結果是一樣的坷襟,意思就是執(zhí)行的流程是一樣的奸柬。但是在具體到每個步驟的時候,存在差異:#import
導入時啤握,直接使用預先編譯好的二進制文件鸟缕。
每次包含標頭時,編譯器都必須可傳遞地預處理和解析該標頭及其包含的每個標頭中的文本排抬。必須對應用程序中的每個翻譯單元重復此過程,這涉及大量的冗余工作授段。
#include
偽指令被預處理程序視為文本包含蹲蒲,因此在包含時必須接受任何活動的宏定義。如果任何活動宏定義碰巧與庫中的名稱沖突侵贵,則可能會破壞庫API或導致庫頭本身的編譯失敗届搁。
此外,導入模塊時將自動提供使用該模塊所需的任何鏈接器標志
每個模塊都被解析為一個獨立的實體窍育,因此它具有一致的預處理器環(huán)境卡睦。
此外,在遇到導入聲明時漱抓,當前的預處理器定義將被忽略表锻,
@import
上面的聲明導入std模塊的全部內容(其中將包含例如整個C或C ++標準庫),并在當前翻譯單元中提供其API乞娄。要僅導入模塊的一部分瞬逊,可以使用點語法來特定特定的子模塊
模塊會自動將#include
指令轉換為相應的模塊導入
關于開銷的問題:
如果只編譯一個C.c文件,A仪或、B确镊、C這3個頭文件都需要編譯一次,兩種導入方式無差異范删。但是真正的項目中蕾域,依賴關系通常都很復雜,使用import做到每個文件只編譯一次到旦,就可以節(jié)省開銷旨巷。
在具有N個翻譯單元和每個翻譯單元中包含M個標頭的項目中廓块,即使M個標頭中的大多數(shù)在多個翻譯單元之間共享,編譯器仍在執(zhí)行M x N個工作契沫。
std.io
模塊僅編譯一次带猴,并且將模塊導入轉換單元是恒定時間操作(與模塊系統(tǒng)無關)。因此懈万,每個軟件庫的API僅解析一次拴清,從而將M x N編譯問題減少為M + N問題。
用modulemap驗證#import
導入方式
參考文檔
https://clang.llvm.org/docs/APINotes.html
https://clang.llvm.org/docs/Modules.html#export-declaration
# -fmodules:允許使用module語言來表示頭文件
# -fmodule-map-file:module map的路徑会通。如不指明默認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之間映射的關系
- 定義了名稱為A和B的兩個module
- 在
module A
中口予,定義了header A.h
,表示module A
和A.h的映射關系 - 在
module B
中涕侈,定義了header B.h
沪停,和A同理。export A
表示將B.h
導入的A.h
頭文件重新導出
用clang指令使用fmodules方式生成目標文件:
module
在Xcode
中是默認開啟的裳涛。如果在Build Settings
中木张,將Enable Modules
設置為NO,導入頭文件將不能使用@import
方式端三。開啟module
后舷礼,項目中導入頭文件,無論使用#include
郊闯、#import
妻献、@import
中何種方式,最終都會映射為@import
方式团赁。
Cocoapod安裝的AFNetworking文件的modulemap
// 聲明framework的module名稱為AFNetworking
framework module AFNetworking {
// 導入文件的集合(如果沒有關鍵字header那么umbrella后面需要跟上頭文件的文件夾名稱)
umbrella header "AFNetworking-umbrella.h"
export * //把引入的頭文件重新導出育拨。
module * { export * } //把導入頭文件修飾成子module,并把符號全部導出(第一個通配符*表示子module名稱和父module名稱一致)
// 如果要指定子module的名稱需要使用explicit關鍵字
// eg:
explicit module NANetworking {
header "NANetworking.h"
export *
}
}
umbrella
:雨傘頭欢摄,可以理解為傘柄熬丧。一把雨傘的傘柄下有很多傘骨,umbrella
的作用是指定一個目錄剧浸,這個目錄即為傘柄锹引,目錄下所有.h
頭文件即為傘骨。
explicit
:顯示指明子module
名稱唆香。
自定義Module
為什么需要用到自定義Module嫌变?
因為在生成一個自定義庫時,在我們的Framework
項目中并沒有幫我們生成ModuleMap
文件躬它,它只會在編譯時自動幫我們生成腾啥。這樣就存在一個問題,如果我們想在項目中配置自己的東西,比如說配置一個子Module
倘待。這種場景下疮跑,我們就需要寫一個自己的Module
文件。
- 寫好一個
Module
文件后凸舵,將項目加到對應Framework
項目的Tartget
中祖娘。 - 配置設置中的
Module Map File
,路徑的設置是以SRCRoot
為前置路徑的啊奄。 - 因為系統(tǒng)默認查找
module.modulemap
文件渐苏。自定義的modulemap
文件,無論什么名稱菇夸,在編譯后琼富,都會把文件名稱改成module.modulemap
文件名。 -
子module
的導出可以用通配符庄新,也可以一個個單獨指定鞠眉。