iOS開發(fā)進(jìn)階八:Module(模塊)

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
Clang預(yù)處理中頭文件處理過程.png

無論文件中是#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文件:

準(zhǔn)備驗(yàn)證modulemap的使用.png

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)文件:

fmodules方式編譯后生成的pcm二進(jìn)制文件.png

moduleXcode中是默認(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ú)指定。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容