module(模块):最小的代码单元
一个module是机器代码和数据的最小单位,可以独立于其他代码单位进行链接。
这句话的的解释:
通常,module是通过编译单个源文件生成的目标文件。例如:当前的test.m被编译成目标文件test.o时,当前的目标文件就代表了一个module。
但是,有一个问题,Module在调用的时候会产生开销,比如我们在使用一个静态库的时候,可以这样使用
@import TestStaticFramework;
这个静态库中可能包含了许多的.o文件。岂不是要导入很多的Module。并不需要。在静态链接的时候,也就是静态库链接到主项目或者动态库时,最后生成可执行文件或者动态库时。静态链接器可以把多个Module链接优化成一个,来减少本来多个Module直接调用的问题。
- 问题一:我们经常讲.m的编译,那.h是怎么编译的?
- 问题二:Module在调用的时候会产生开销,比如我们在使用一个静态库的时候。这种开销是怎么引起的?系统是怎么优化这种开销?
- 问题三:通常我们在项目中使用
#import
导入AFN,实际上需要导入很多个头文件,这其中产生了什么问题?当我们在代码中导入一个库
或者引入其它头文件
的时候,发生了什么事情? - 问题四:ModuleMap是什么?
#include
与#import
差异
案例:现存在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
预编译出来的结果是一样的,意思就是执行的流程是一样的。但是在具体到每个步骤的时候,存在差异:#import
导入时,直接使用预先编译好的二进制文件。
每次包含标头时,编译器都必须可传递地预处理和解析该标头及其包含的每个标头中的文本。必须对应用程序中的每个翻译单元重复此过程,这涉及大量的冗余工作。
#include
伪指令被预处理程序视为文本包含,因此在包含时必须接受任何活动的宏定义。如果任何活动宏定义碰巧与库中的名称冲突,则可能会破坏库API或导致库头本身的编译失败。
此外,导入模块时将自动提供使用该模块所需的任何链接器标志
每个模块都被解析为一个独立的实体,因此它具有一致的预处理器环境。
此外,在遇到导入声明时,当前的预处理器定义将被忽略,
@import
上面的声明导入std模块的全部内容(其中将包含例如整个C或C ++标准库),并在当前翻译单元中提供其API。要仅导入模块的一部分,可以使用点语法来特定特定的子模块
模块会自动将#include
指令转换为相应的模块导入
关于开销的问题:
如果只编译一个C.c文件,A、B、C这3个头文件都需要编译一次,两种导入方式无差异。但是真正的项目中,依赖关系通常都很复杂,使用import做到每个文件只编译一次,就可以节省开销。
在具有N个翻译单元和每个翻译单元中包含M个标头的项目中,即使M个标头中的大多数在多个翻译单元之间共享,编译器仍在执行M x N个工作。
std.io
模块仅编译一次,并且将模块导入转换单元是恒定时间操作(与模块系统无关)。因此,每个软件库的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
为前置路径的。 - 因为系统默认查找
module.modulemap
文件。自定义的modulemap
文件,无论什么名称,在编译后,都会把文件名称改成module.modulemap
文件名。 -
子module
的导出可以用通配符,也可以一个个单独指定。