Module(模块)
-
Module(模块): 最小的代码单元。
一个Module是机器代码和数据的最小单位,可以独立于其他代码单位进行链接。通常,Module是通过编译单个源文件生成的目标文件。
例如:当前的test.m被编译成目标文件test.o,当前的目标文件就代表一个Module。但是有一个问题,Module在调用的时候会产生开销,比如我们在使用一个静态库的时候可以这样使用:
@import TestStaticFramework;
这个静态库中可能包含了许多的.o文件。岂不是要导入很多的Module?
并不需要,在静态链接的时候,也就是静态库链接到主项目或者动态库,最后生成可执行文件或者动态库时,静态链接器可以把多个Module链接优化成一个,来减少原本多个Module直接调用的问题。
-
module.modulemap用来描述头文件与module之间的映射关系。
下面使我们经常用到的AFNetworking产生的.modulemap文件:
image.png
那么.modulemap里面的这些代码又是什么意思呢?
/// 声明一个module A,它映射的头文件是 A.h
module A {
header "A.h"
}
/// 声明一个module B,它映射的头文件是 B.h
module B {
header "B.h"
/// 导出 A,这里假设 "B.h" 映入了 "A.h";
/// 那么 导出 的意思就是将"B.h"引入的其他的"头文件" 也暴露出来。
export A
}
---
通常我们看到的`module`里面是 {export *} ,如上面的`AFNetworking.modulemap`
"*" 是通配符,意思是所有引入的`头文件`,全部 导出。
我们来读一下AFNetworking.modulemap:
/// framework module 名称 AFNetworking
framework module AFNetworking {
/// umbrella <目录> 伞柄 <目录>/.h
/// AFNetworking-umbrella.h 伞柄
/// AFNetworking-umbrella.h/.h 伞骨(里面引入的所有的其他头文件)
umbrella header "AFNetworking-umbrella.h"
/// 重新导出
export *
/// module: 子module*
module * { export * }
}

如果要显示指明
子module的名称,要加上explicit关键字:
/// 假设在 `AFNetworking.modulemap` 中显示指明 `子module`
framework module AFNetworking {
/// umbrella <目录> 伞柄 <目录>/.h
/// AFNetworking-umbrella.h 伞柄
/// AFNetworking-umbrella.h/.h 伞骨(里面引入的所有的其他头文件)
umbrella header "AFNetworking-umbrella.h"
/// 重新导出
export *
/// module: 子module*
module * { export * }
/// 假设 `子module` 叫 `SubAFN`
explicit module SubAFN {
header "SubAFN.h"
export *
}
}
- 我们的Xcode是自动开启
Module的
所有开发中我们引入头文件的三种形式:
#include、#import、@import最终都会被转换成@import。
image.png -
Module到底有什么用呢?
在我们传统的#include引入头文件,并且没有开启Module的情况下。头文件被引入多少次就要被编译所少次。比如:
A.h,此时被use.c和use_1.c引入,那么此时A.h就要被编译两次。
此时Module的好处就提现出来了。它会预先把A.h编译成二进制文件,那么后面不管有多少个文件使用到A.h,只有一个A.h的二进制文件(除非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的路径。如不指明默认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:

可以看到生成了两个二级制文件,这就是我们头文件预先编译的产物:

Swift库
-
我们都知道,在平常的开发中,
swift和OC代码的混编,我们都是使用桥接文件(Bridging-header)。
但是在Framework中,是不允许使用桥接文件的。因此:我们不能使用桥接文件的方式进行混编Objective-C代码的引用,需要用Swift Module进行模块间的引用。
i:首先创建自己的.modulemap文件:
image.png
ii:在Framework的Build Settings里面配置Module Map File:
image.png
iii:此时已经可以在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;
这样做虽然做不到完全的隐藏,但是可以达到提醒使用者的作用,告诉用户这是一个私有库,不要随便使用。
这里还有另外一种方法可以达到上面的要求:
swift和OC遵守同一个协议,通过协议来调用OC的代码,从而做到隐藏OC代码的效果。
Swift Module
- Xcode 9 之后,Swift 开始支持静态库。
Swift 没有头文件的概念,那么我们外界要使用 Swift中用public修饰的类和函数该怎么办呢?
Swift库中引入了一个全新的文件.swiftmodule。
.swiftmodule包含序列化过的AST(抽象语法树,Abstract Syntax Tree),也包含STL(Swift 中间语言,Swift Intermediate Language)。
比如我们刚刚的工程中,Xcode就给我们自动生成了.swiftmodule文件。
我们要怎么用Swift Module呢?下面我们通过swift静态库来看一下。
Swift 静态库的合并
- 1、首先我们创建
SwiftA&SwiftB两个静态库,并且两个静态库中都包含YSSwiftTeacher,名字和函数一模一样(埋点,看看后面会不会报错):
image.png - 我们将生成的库文件拷贝出来,做一下合并:
libtool -static SwiftA SwiftB -o libSwiftC.a
此时会有一个警告,提示我们合并的两个库文件中有相同的文件。

这就是我们使用
libtool的好处,我们之前讲过可以使用ar来合并静态库,但是使用ar的情况下,先解压再合并,可能发生文件的替换。我们来查看下当前静态库里面有哪些
.o文件:
可以看到,两个库里面的同名
.o文件都在,并没有产生替换。
- 2、接下来我们将
.framework里面原先的签名文件,配置文件删除,只留下Headers和Modules。因为我们需要的是合并后的静态库,所有原先的签名和配置没有用了。
image.png - 3、将
Modules里面的文件,提到和``Headers一个等级,这样做是为了后续SwiftC编译器能够找到对应的Module文件。要不然可能会找不到。
image.png - 4、将我们合并后的静态库
libSwiftC.a拖到工程里面,并且配置xcconfig文件
image.png
HEADER_SEARCH_PATHS = $(inherited) "SwiftC/SwiftA.framework/Headers" "SwiftC/SwiftB.framework/Headers"
// OTHER_CFLAGS: 传递给 用来编译C或者OC的编译器,当前就是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"
这里强调一下: 配置
OTHER_CFLAGS是为了给OC代码用;配置SWIFT_INCLUDE_PATHS是为了给Swift代码用。
image.png
- 5、编译运行我们发现两点不同:
i:OC文件中同时使用A和B没有问题:
image.png
ii:Swift文件中不允许这样使用:
image.png
这也与两门语言的特性有关系,
OC是运行时语言,我们已经告诉编译器,头文件的地址,所以只要运行时能够找到对应的符号就不会报错。但是Swift就不一样,在编译的时候检测到可能存在的隐患就会报错。
另外,不管是静态库还是动态库的合并,大家尽量用不同的文件夹隔开要合并的库的库文件,这样可以预防Header里面有相同的文件(也就是我们上面的埋点)。
Swift配置
在我们日常的开发过程中,Swift去使用OC的一些方法的时候,Swift会进行一些优化。
比如:
- 函数 :
/// OC 定义
- (void)ysOCFuncationWithValue:(NSString *)value WithKey:(NSString *)key;
/// Swift 使用
let obj = YSObject.init()
obj.ysOCFuncation(withValue: "123", withKey: "key")
/**************此时我们如果想要规范一下Swift中的函数名可以这样*****************/
- (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从现在开始,Swift的Clang Importer将做一些额外的工作并将该方法导入为私有方法,并以双下划线字符开头__,例如:
//- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options;
- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options
NS_REFINED_FOR_SWIFT;
- 上面的写法虽然可行,但是也存在一些弊端。如果我们要大批量的去修改已经稳定的
OC的库的时候,就会是一个繁重的工作。 - 此时我们可以使用
.apinotes文件来惊醒修改。
命名规则: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 // 设置在Swift中不能用
AvailabilityMsg: "这个不能用" // 提示
- Selector: "initWithName:"
MethodKind: Instance
DesignatedInit: true

这个时候我们就可以结合脚本批量修改。
另外还有很多的用法,可以参考:API Notes












