關(guān)鍵詞:#import
,#include
,@class
,Modules
,預(yù)處理(preprocessor)
一氓拼、概述
#include是C/C++導(dǎo)入頭文件的關(guān)鍵字寞秃;
#import是Objective-C導(dǎo)入頭文件關(guān)鍵字;
@class告訴編譯器某個(gè)類(lèi)的聲明世蔗,當(dāng)執(zhí)行時(shí)嚎幸,才去查看類(lèi)的實(shí)現(xiàn)文件,可以解決頭文件的相互包含的問(wèn)題洗贰;
二找岖、#import"xxx.h" 和 #import<xxx/xxx.h> 的區(qū)別
1)區(qū)別
#import<xxx/xxx.h>
: 引用系統(tǒng)文件,它用于對(duì)系統(tǒng)自帶的頭文件的引用敛滋,編譯器會(huì)在系統(tǒng)文件目錄下去查找該文件许布。
#import"xxx.h"
: 用戶(hù)自定義的文件用雙引號(hào)引用,編譯器首先會(huì)在用戶(hù)目錄下查找绎晃,然后到安裝目錄中查蜜唾。
雙引號(hào)是用于本地的頭文件,需要指定相對(duì)路徑庶艾,尖括號(hào)是全局的引用袁余,其路徑由編譯器提供,如引用系統(tǒng)的庫(kù)咱揍。
2)use header map
Enable the use of Header Maps, which provide the compiler with a mapping from textual header names to their locations, bypassing the normal compiler header search path mechanisms. This allows source code to include headers from various locations in the file system without needing to update the header search path build settings。
意思是Xcode開(kāi)啟這個(gè)開(kāi)關(guān)后,在本地會(huì)根據(jù)當(dāng)前目錄生成一份文件名和相對(duì)路徑的映射碗淌,依靠這個(gè)映射,我們可以直接import工程里的文件噪漾,不需要依靠header search path。
如果關(guān)閉use header map
那么就涉及到xcode build settings中的header search path
和 user header search path
了且蓬。
兩者都是提供search path的欣硼,區(qū)別在于一個(gè)指明是用戶(hù)的。并且提到如果編譯器不支持user headers概念缅疟,會(huì)從header search paths中去尋找分别。
三、存在的問(wèn)題
傳統(tǒng)的#include/#import都是文本語(yǔ)義: 預(yù)處理器在處理的時(shí)候會(huì)把這一行替換成對(duì)應(yīng)頭文件的文本存淫。
這樣就會(huì)導(dǎo)致以下問(wèn)題:
問(wèn)題1)大量的預(yù)處理消耗耘斩;
假如有N個(gè)頭文件,每個(gè)頭文件又#include了M個(gè)頭文件桅咆,那么整個(gè)預(yù)處理的消耗是N*M括授。這可能會(huì)在你的頭文件里面引入數(shù)量非常龐大的代碼。假如換成 C++岩饼,那情況就更糟了荚虚,因?yàn)樗€包含了一些模板代碼,數(shù)量比 C 還要多好幾倍籍茧;
問(wèn)題2)文件導(dǎo)入后版述,宏定義容易出現(xiàn)問(wèn)題;
因?yàn)槭俏谋緦?dǎo)入寞冯,并且按照include依次替換渴析,當(dāng)一個(gè)頭文件定義了#define std hello_world,而第另一個(gè)頭文件剛好又是C++標(biāo)準(zhǔn)庫(kù)吮龄,那么include順序不同俭茧,可能會(huì)導(dǎo)致所有的std都會(huì)被替換。那最終運(yùn)行時(shí)結(jié)果就出乎意料了漓帚,這使得預(yù)處理變得非衬刚“脆弱”;
問(wèn)題3)邊界不明顯尝抖;
什么時(shí)候用什么工具毡们、庫(kù)來(lái)開(kāi)發(fā)軟件,僅僅從頭文件上面看其實(shí)你并不能看得懂昧辽,因?yàn)樗⒉皇恰罢Z(yǔ)義化”的漏隐,比如哪些命名空間屬于特定的庫(kù),比如拿到一組.a和.h文件奴迅,很難確定.h是屬于哪個(gè).a的青责?這些命名空間又該以如何的順序包含挺据,需要以什么樣的順序?qū)氩拍苷_編譯?或者你又只想引入這個(gè)庫(kù)的一部分定義脖隶,只通過(guò) “#include”之類(lèi)的預(yù)處理真的是太難搞清楚了扁耐;
問(wèn)題4)引用泛濫;
A 類(lèi)導(dǎo)入了 C 類(lèi)的頭文件产阱,B 類(lèi)也導(dǎo)入了 C 類(lèi)的頭文件婉称,D 類(lèi)又同時(shí)導(dǎo)入 A 和 B 類(lèi),這就是重復(fù)導(dǎo)入;
問(wèn)題5)交叉引用构蹬;
如果有循環(huán)依賴(lài)關(guān)系王暗,如:A–>B, B–>A這樣的相互依賴(lài)關(guān)系。如:
當(dāng)在TestA類(lèi)的.h中 #import "TestB.h" 并聲明TestB屬性時(shí)報(bào)錯(cuò):
Unknown type name 'TestB'; did you mean 'TestA'?
解決辦法:在.h文件中使用@class
庄敛,然后在.m文件中#import "TestB.h"
即可俗壹。
原因: @class TestB;
這句話(huà)的意思就是,告訴編譯器藻烤,確實(shí)有TestB這個(gè)類(lèi)绷雏,具體細(xì)節(jié)你不用管,別報(bào)錯(cuò)就行了怖亭。所以顯然涎显,到了.m里,它只知道有這個(gè)類(lèi)兴猩,卻不知道這個(gè)類(lèi)有什么屬性期吓,有哪些方法。所以需要在.m再 import這個(gè)頭文件倾芝。
【注意】在.m文件中#import "TestB.h"
并聲明TestB屬性是不會(huì)報(bào)錯(cuò)的讨勤;
盡量在.m而不是.h里使用import引用
推薦盡量在.m里引用頭文件,而不是在.h里蛀醉,必要時(shí)使用@class
。
在編譯效率方面考慮衅码,如果你有100個(gè)頭文件都#import了同一個(gè)頭文件拯刁,或者這些文件是依次引用的,如A–>B, B–>C, C–>D這樣的引用關(guān)系逝段。當(dāng)最開(kāi)始的那個(gè)頭文件有變化的話(huà)垛玻,后面所有引用它的類(lèi)都需要重新編譯,如果你的類(lèi)有很多的話(huà)奶躯,這將耗費(fèi)大量的時(shí)間帚桩。而是用
@class
則不會(huì)。
但是也有一些情況嘹黔,是不可避免要在.h里引用的账嚎。比如:繼承某個(gè)類(lèi)莫瞬,必須在.h里 import 父類(lèi)的.h;類(lèi)實(shí)現(xiàn)某個(gè)接口郭蕉,必須在.h里引用接口的.h等等疼邀。
四、Clang Module
Modules 是一種語(yǔ)義化的引入定義的方法召锈。
clang module不再使用文本模型, 而是采用更高效的語(yǔ)義模型旁振。clang module提供了一種新的導(dǎo)入方式:@import
,module會(huì)被作為一個(gè)獨(dú)立的模塊編譯涨岁,并且產(chǎn)生獨(dú)立的緩存拐袜,從而大幅度提高預(yù)處理效率,這樣時(shí)間消耗從M*N變成了M+N梢薪。
// Swift
@import WebKit.WebKitLegacy; //in Objective-C
import WebKit.WebKitLegacy //in Swift
// OC
#import "TestA.h"
#import <WebKit/WebKit.h>
可以看到 Objective-C 和 Swift 都非常好地支持了 Modules import蹬铺,你可以非常清晰地引入 API 聲明。
當(dāng)你使用 Modules 引入時(shí)沮尿,預(yù)處理器并不會(huì)像 “#include”那樣使用 M*N 量級(jí)的重復(fù)拷貝粘貼丛塌。而是巧妙地通過(guò)一個(gè)列表來(lái)存放已經(jīng)編譯處理過(guò)的 Modules 列表,而聲明的引入會(huì)首先在這個(gè)表內(nèi)查找畜疾,如果沒(méi)有找到會(huì)去編譯添加進(jìn)來(lái)赴邻。所以 Modules 的引入只會(huì)被處理一次,可以解決前面提到的引用泛濫問(wèn)題啡捶。
自 Xcode5以來(lái)姥敛,build settings 都默認(rèn)開(kāi)啟了 “-fmodules”,一般來(lái)講你的代碼里面都可以使用 Modules 來(lái)引入其他庫(kù)瞎暑。其實(shí) modules 是一種頭文件編譯后的 map彤敛,所以 Modules 始終都能保證你所引入的定義是存在的、有意義的了赌。(其實(shí) Modules 是一種從 precompile headers 演變過(guò)來(lái)的技術(shù))
modules 和 headers 通過(guò)一個(gè) map 來(lái)進(jìn)行一種關(guān)系映射墨榄,這個(gè) map 文件就叫做 modulemap. 這個(gè)文件從語(yǔ)義上描述了你的函數(shù)庫(kù)物理結(jié)構(gòu)。
舉個(gè)例子勿她,用 std 這個(gè) module 來(lái)描述 C 的標(biāo)準(zhǔn)庫(kù)袄秩。那么 C 標(biāo)準(zhǔn)庫(kù)里面的那些頭文件:stdio.h, stdlib.h, math.h 都可以映射到 std 這個(gè) module 里面,他們就組成了幾個(gè) 子模塊(submodule): std.io, std.lib, std.math逢并。通過(guò)這樣一個(gè)映射關(guān)系之剧,C 的標(biāo)準(zhǔn)庫(kù)就可以構(gòu)建出一個(gè)獨(dú)立的 module。所以通常地砍聊,一個(gè)庫(kù)就只有一個(gè) module.modulemap 文件用于描述它的所有頭文件映射背稼。
那么實(shí)際在編譯過(guò)程中 Modules 到底代表著什么呢?我們前面說(shuō)過(guò)其實(shí) Modules 是一種預(yù)編譯技術(shù)玻蝌,當(dāng)一個(gè)模塊被導(dǎo)入時(shí)蟹肘,編譯器在處理它時(shí)會(huì)生成一個(gè)新的子進(jìn)程(非 fork)词疼,這個(gè)子進(jìn)程擁有干凈的 context來(lái)編譯這個(gè) module(這樣就不會(huì)產(chǎn)生命名空間沖突等干擾),然后 module 的編譯結(jié)果會(huì)被持久化到這個(gè)模塊的二進(jìn)制緩存中疆前,那么下次引用編譯的時(shí)候就會(huì)非澈快。 modules 由頭文件映射而成竹椒,所以當(dāng)這些頭文件改動(dòng)時(shí)童太,module 還會(huì)自動(dòng)重新編譯刷新緩存,不需要我們主動(dòng)干預(yù)胸完。
相關(guān)參考
1)LLVM的 Modules https://www.stephenw.cc/2017/08/23/llvm-modules/
2)關(guān)于Objective-C中的import https://juejin.im/post/58d88c7ab123db199f442aec
3)相互引用頭文件問(wèn)題 https://juejin.im/post/5aaf6943518825556e5de48e