Module(模塊)
-
Module(模塊): 最小的代碼單元。
一個Module是機器代碼和數(shù)據(jù)的最小單位,可以獨立于其他代碼單位進(jìn)行鏈接。通常,Module是通過編譯單個源文件生成的目標(biāo)文件。
例如:當(dāng)前的test.m被編譯成目標(biāo)文件test.o,當(dāng)前的目標(biāo)文件就代表一個Module。但是有一個問題,Module在調(diào)用的時候會產(chǎn)生開銷,比如我們在使用一個靜態(tài)庫的時候可以這樣使用:
@import TestStaticFramework;
這個靜態(tài)庫中可能包含了許多的.o文件。豈不是要導(dǎo)入很多的Module?
并不需要,在靜態(tài)鏈接的時候,也就是靜態(tài)庫鏈接到主項目或者動態(tài)庫,最后生成可執(zhí)行文件或者動態(tài)庫時,靜態(tài)鏈接器可以把多個Module鏈接優(yōu)化成一個,來減少原本多個Module直接調(diào)用的問題。
-
module.modulemap用來描述頭文件與module之間的映射關(guān)系。
下面使我們經(jīng)常用到的AFNetworking產(chǎn)生的.modulemap文件:
image.png
那么.modulemap里面的這些代碼又是什么意思呢?
/// 聲明一個module A,它映射的頭文件是 A.h
module A {
header "A.h"
}
/// 聲明一個module B,它映射的頭文件是 B.h
module B {
header "B.h"
/// 導(dǎo)出 A,這里假設(shè) "B.h" 映入了 "A.h";
/// 那么 導(dǎo)出 的意思就是將"B.h"引入的其他的"頭文件" 也暴露出來。
export A
}
---
通常我們看到的`module`里面是 {export *} ,如上面的`AFNetworking.modulemap`
"*" 是通配符,意思是所有引入的`頭文件`,全部 導(dǎo)出。
我們來讀一下AFNetworking.modulemap:
/// framework module 名稱 AFNetworking
framework module AFNetworking {
/// umbrella <目錄> 傘柄 <目錄>/.h
/// AFNetworking-umbrella.h 傘柄
/// AFNetworking-umbrella.h/.h 傘骨(里面引入的所有的其他頭文件)
umbrella header "AFNetworking-umbrella.h"
/// 重新導(dǎo)出
export *
/// module: 子module*
module * { export * }
}

如果要顯示指明
子module的名稱,要加上explicit關(guān)鍵字:
/// 假設(shè)在 `AFNetworking.modulemap` 中顯示指明 `子module`
framework module AFNetworking {
/// umbrella <目錄> 傘柄 <目錄>/.h
/// AFNetworking-umbrella.h 傘柄
/// AFNetworking-umbrella.h/.h 傘骨(里面引入的所有的其他頭文件)
umbrella header "AFNetworking-umbrella.h"
/// 重新導(dǎo)出
export *
/// module: 子module*
module * { export * }
/// 假設(shè) `子module` 叫 `SubAFN`
explicit module SubAFN {
header "SubAFN.h"
export *
}
}
- 我們的Xcode是自動開啟
Module的
所有開發(fā)中我們引入頭文件的三種形式:
#include、#import、@import最終都會被轉(zhuǎn)換成@import。
image.png -
Module到底有什么用呢?
在我們傳統(tǒng)的#include引入頭文件,并且沒有開啟Module的情況下。頭文件被引入多少次就要被編譯所少次。比如:
A.h,此時被use.c和use_1.c引入,那么此時A.h就要被編譯兩次。
此時Module的好處就提現(xiàn)出來了。它會預(yù)先把A.h編譯成二進(jìn)制文件,那么后面不管有多少個文件使用到A.h,只有一個A.h的二進(jìn)制文件(除非A.h被改動)。
下面我們來演示一下:
1、module.modulemap:
module A {
header "A.h"
}
module B {
header "B.h"
export A
}
2、終端指令:
# -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
3、B.h 引用 A.h, use.c 引用 B.h:

可以看到生成了兩個二級制文件,這就是我們頭文件預(yù)先編譯的產(chǎn)物:

Swift庫
-
我們都知道,在平常的開發(fā)中,
swift和OC代碼的混編,我們都是使用橋接文件(Bridging-header)。
但是在Framework中,是不允許使用橋接文件的。因此:我們不能使用橋接文件的方式進(jìn)行混編Objective-C代碼的引用,需要用Swift Module進(jìn)行模塊間的引用。
i:首先創(chuàng)建自己的.modulemap文件:
image.png
ii:在Framework的Build Settings里面配置Module Map File:
image.png
iii:此時已經(jīng)可以在swift文件中使用OC的代碼了(同時OC文件中使用swift代碼也是一樣的):
image.png 這樣就引出了另外一個問題:如果我不想將
YSStudent暴露到Framework之外怎么辦?
這個時候我們就可以引入.private.modulemap文件
framework module YSSwiftFramework_Private {
module YSOCStudent {
header "YSOCStudent.h"
export *
}
}
-
注意:①
_Private首字母大寫,② 文件名中要有.private。
然后配置一下Build Settings:
image.png - 注意:這里雖然配置了
.private.modulemap文件,但并不是真正的隱藏。我們通過模塊訪問,依然是可以訪問到的:
@import YSSwiftFramework_Private.YSOCStudent;
這樣做雖然做不到完全的隱藏,但是可以達(dá)到提醒使用者的作用,告訴用戶這是一個私有庫,不要隨便使用。
這里還有另外一種方法可以達(dá)到上面的要求:
swift和OC遵守同一個協(xié)議,通過協(xié)議來調(diào)用OC的代碼,從而做到隱藏OC代碼的效果。
Swift Module
- Xcode 9 之后,Swift 開始支持靜態(tài)庫。
Swift 沒有頭文件的概念,那么我們外界要使用 Swift中用public修飾的類和函數(shù)該怎么辦呢?
Swift庫中引入了一個全新的文件.swiftmodule。
.swiftmodule包含序列化過的AST(抽象語法樹,Abstract Syntax Tree),也包含STL(Swift 中間語言,Swift Intermediate Language)。
比如我們剛剛的工程中,Xcode就給我們自動生成了.swiftmodule文件。
我們要怎么用Swift Module呢?下面我們通過swift靜態(tài)庫來看一下。
Swift 靜態(tài)庫的合并
- 1、首先我們創(chuàng)建
SwiftA&SwiftB兩個靜態(tài)庫,并且兩個靜態(tài)庫中都包含YSSwiftTeacher,名字和函數(shù)一模一樣(埋點,看看后面會不會報錯):
image.png - 我們將生成的庫文件拷貝出來,做一下合并:
libtool -static SwiftA SwiftB -o libSwiftC.a
此時會有一個警告,提示我們合并的兩個庫文件中有相同的文件。

這就是我們使用
libtool的好處,我們之前講過可以使用ar來合并靜態(tài)庫,但是使用ar的情況下,先解壓再合并,可能發(fā)生文件的替換。我們來查看下當(dāng)前靜態(tài)庫里面有哪些
.o文件:
可以看到,兩個庫里面的同名
.o文件都在,并沒有產(chǎn)生替換。
- 2、接下來我們將
.framework里面原先的簽名文件,配置文件刪除,只留下Headers和Modules。因為我們需要的是合并后的靜態(tài)庫,所有原先的簽名和配置沒有用了。
image.png - 3、將
Modules里面的文件,提到和``Headers一個等級,這樣做是為了后續(xù)SwiftC編譯器能夠找到對應(yīng)的Module文件。要不然可能會找不到。
image.png - 4、將我們合并后的靜態(tài)庫
libSwiftC.a拖到工程里面,并且配置xcconfig文件
image.png
HEADER_SEARCH_PATHS = $(inherited) "SwiftC/SwiftA.framework/Headers" "SwiftC/SwiftB.framework/Headers"
// OTHER_CFLAGS: 傳遞給 用來編譯C或者OC的編譯器,當(dāng)前就是clang
OTHER_CFLAGS="-fmodule-map-file=${SRCROOT}/SwiftC/SwiftA.framework/module.modulemap" "-fmodule-map-file=${SRCROOT}/SwiftC/SwiftB.framework/module.modulemap"
// SWIFT_INCLUDE_PATHS: 傳遞給SwiftC編譯器,告訴它去下面的路徑查找module.file
SWIFT_INCLUDE_PATHS="${SRCROOT}/SwiftC/SwiftA.framework" "${SRCROOT}/SwiftC/SwiftB.framework"
這里強調(diào)一下: 配置
OTHER_CFLAGS是為了給OC代碼用;配置SWIFT_INCLUDE_PATHS是為了給Swift代碼用。
image.png
- 5、編譯運行我們發(fā)現(xiàn)兩點不同:
i:OC文件中同時使用A和B沒有問題:
image.png
ii:Swift文件中不允許這樣使用:
image.png
這也與兩門語言的特性有關(guān)系,
OC是運行時語言,我們已經(jīng)告訴編譯器,頭文件的地址,所以只要運行時能夠找到對應(yīng)的符號就不會報錯。但是Swift就不一樣,在編譯的時候檢測到可能存在的隱患就會報錯。
另外,不管是靜態(tài)庫還是動態(tài)庫的合并,大家盡量用不同的文件夾隔開要合并的庫的庫文件,這樣可以預(yù)防Header里面有相同的文件(也就是我們上面的埋點)。
Swift配置
在我們?nèi)粘5拈_發(fā)過程中,Swift去使用OC的一些方法的時候,Swift會進(jìn)行一些優(yōu)化。
比如:
- 函數(shù) :
/// OC 定義
- (void)ysOCFuncationWithValue:(NSString *)value WithKey:(NSString *)key;
/// Swift 使用
let obj = YSObject.init()
obj.ysOCFuncation(withValue: "123", withKey: "key")
/**************此時我們?nèi)绻胍?guī)范一下Swift中的函數(shù)名可以這樣*****************/
- (void)ysOCFuncationWithValue:(NSString *)value WithKey:(NSString *)key
NS_SWIFT_NAME(ysAction(key:value:));
/// Swift 使用
let obj = YSObject.init()
obj.ysAction(key: "key", value: "123")
如果想要定義私有方法:
// NS_REFINED_FOR_SWIFT從現(xiàn)在開始,Swift的Clang Importer將做一些額外的工作并將該方法導(dǎo)入為私有方法,并以雙下劃線字符開頭__,例如:
//- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options;
- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options
NS_REFINED_FOR_SWIFT;
- 上面的寫法雖然可行,但是也存在一些弊端。如果我們要大批量的去修改已經(jīng)穩(wěn)定的
OC的庫的時候,就會是一個繁重的工作。 - 此時我們可以使用
.apinotes文件來驚醒修改。
命名規(guī)則:SDK名稱.apinotes。并且該文件一定要放到SDK的目錄里面去。
Name: OCFramework // SDK 名稱
Classes:
- Name: LGToSwift // 類名
SwiftName: ToSwift // 在Swift中的名稱
Methods: // 方法
- Selector: "changeTeacherName:" // 方法名
Parameters:
- Position: 0
Nullability: O
MethodKind: Instance
SwiftPrivate: true
Availability: nonswift // 設(shè)置在Swift中不能用
AvailabilityMsg: "這個不能用" // 提示
- Selector: "initWithName:"
MethodKind: Instance
DesignatedInit: true

這個時候我們就可以結(jié)合腳本批量修改。
另外還有很多的用法,可以參考:API Notes












