头文件
创建一个项目,默认生成
ViewController文件,里面有个#import,那么这个#import到底干了什么?
包含头文件的方式有两种:#import、#include,都把.h文件直接拷贝一份到你引用的源码里面。
编译的时候,编译器会帮我们设置头文件,也就是header search path,里面是一组目录,这里就是项目要使用的头文件的目录。当import或者include的时候,会把后面的内容和一组目录拼接到一起组成完整路径,然后把完整路径的内容拷贝到当前.m文件里面。
区别:
import:编译时保存一个字典,每次引入头文件的时候,都会先从字典里面看有没有,如果没有,从路径引入,如果有了就不会再引入了。
include:每次include就会拷贝一份,无论重复与否。
include = import + pragma once
源码是如何变成二进制的?
源码 -> 词法语法分析 -> 生成IR(Bitcode) -> MIR -> 汇编 -> 二进制(.o)
clang:前端,分析词法语法
opt:进行path优化
llc:汇编化
头文件的拷贝就在source code -> Lexer中间的阶段,也就是预处理阶段。
xcode中,有个功能就是预处理:
点击就可以看到预处理就会把.h拷贝到.m中:
xcode的编译单元是.m,每个.m生成一个.o最后再链接一起生成可执行文件。
这里面有个问题,如果我们有100个文件都使用了ViewController,那么这100个文件都要#import "ViewController",.h文件就要拷贝100次,要进行100次的词法语法分析。重复进行的100次操作就会占编译时间,那能不能只进行一次呢?
头文件演变最早是.h,后来发现多文件导入同一个头文件的话,操作会有些冗余,演变出来pch文件->预编译头文件。
pch的作用是先把.h文件生成ast语法树,用的时候把生成好的ast复制过去。
pch头文件预编译,会在编译产物目录生成一个.gch的文件,这个文件就是ast产物的文件,这个就是bitcode二进制文件。
bitcode有两种格式:字节流(二进制)格式和LLVM IR格式。
LLVM IR包含两种格式:Bitcode(.bc)和Text Format(.ll)。
两种格式可以互相转换:
llvm-dis:.bc -> .ll
llvm-asm:.ll -> .bc
两种文件都是全量无损文件,可在此阶段进行优化。如果需要做优化,或者修改bitcode,或者编写pass,都需要熟悉IR。
pch文件
pch只是解决了生成到ast的流程,之后复制,生成bitcode生成汇编还是要做重复操作。它的可见性是全局的,不需要导入,你有多少文件,这些文件都会把pch引入进来,有些本身不需要的它也会引用,造成另一种浪费。它是全局可见性,不需要导入。
为了更好理解pch,我们创建个工程,添加PrefixHeader文件,要使这个文件生效,需要在Build Settings里面设置对应环境变量。搜索prefix:
找到这两个环境变量名称,创建Config.xcconfig,设置对应环境变量:
GCC_PRECOMPILE_PREFIX_HEADER = YES
GCC_PREFIX_HEADER = "${SRCROOT}/pchTest/PrefixHeader.pch"
在project的info中设置config,进行编译:
可以看到虽然提示build successed了,但是上面还是有个报错:
说是找不到这个文件,这个报错是因为pch这个参数的传递,不能使用SRCROOT,根本识别不出来,第二个是不能加双引号,修改如下:
GCC_PRECOMPILE_PREFIX_HEADER = YES
GCC_PREFIX_HEADER = pchTest/PrefixHeader.pch
验证可见性:在pch文件定义一个函数,这个函数就是全局函数:
void sj_test() {
}
如果在其他文件也定义这个函数,就会报错:
全局函数只有一个,酒泉不重新定义这个函数,多个文件都引入pch,编译的时候,也会报错,这个报错阶段是在链接阶段:
pch优点:提前生成ast的二进制,提升编译效率
pch缺点:可见性是全局可见,如果pch过大,会导致编译变慢
module
那能不能只有需要的才引入,不需要的不引入呢?
module就可以实现这个功能,描述了一组预编译头文件,你哪需要就导入到哪,其他的也不会多拷贝。
module是怎么管理一组头文件的呢,首先关键字module声明一个模块,如果是framework模块可以在前面加framework:
// 定义一个模块的名称Zoo
framework module Zoo {
}
在模块中可以接着定义子模块,模块中头文件用header声明:
module Zoo {
module Sj1 {
header "sj_1.h"
}
module Sj2 {
header "sj_2.h"
}
}
如果sj_1.h中引入了其他的module,如果想把使用的模块也暴露出来,使用export *进行导出。
module Sj1 {
header "sj.h"
export *
module * { export * }
}
module * { export * }的作用是把sj.h里面所有引用的头文件,会把.h去掉,只取前面的名称,通过通配符给定义成子模块。
我们可以使用子模块定义,也可以用一个头文件进行映射,用一个头文件使用umbrella,使用一个头文件去映射多个头文件。
module Zoo {
umbrella header "sj.h"
export *
}
module导入头文件有个好处就是如果我们使用@import Zoo引入,里面的头文件你使用哪个才会引入哪个,如果没使用,它也不会导入。
优化要点:
- 将App代码尽量分散到组件中,减少
pch使用。分散到组件中我们就可以使用module - 尽量使用
framework,会自动生成module - 对
.a开启module,需要我们自己手动配置 - 对遗漏的无法正常使用
module映射的引入hmap
xcode是默认开启module功能,Build Setting里面有个Other C Flags编译C或者OC的时候,给clang传递参数使用;同理Other C++ Flags就是编译C++给clang传递参数使用。
在config文件中配置OTHER_CFLAGS:OTHER_CFLAGS = -fmodule-map-file="${SRCROOT}/pchTest/sj/module.modulemap"。
这里面有个点,就是module文件需要在同一个文件夹中,否则会有问题。
配置完之后,就可以使用@import进行模块导入:
#import "ViewController.h"
@import Zoo;
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
sj_1 *s = [sj_1 new];
}
@end
现在有个问题,在ViewController中使用#import "sj_1.h"导入和在sj_1.m中使用#import "sj_1.h"导入是不是一样的呢?
使用预编译,可以发现在ViewController中使用的直接复制.h文件,而sj_1.m中使用module的方式引入。
在导入头文件的时候,实际上是循环搜索,第一次循环的流程如下:
- 编译文件同级目录去查找头文件信息,有没有和它同名的
.h文件,有没有modulemap文件,如果有modulemap文件会找引入的头文件是不是模块里面的文件,如果是就会用module方式引入;如果不是就用它的同名.h文件,如果都没有,下一步 - 看是不是
framework - 查找
headermap文件,看里面有没有这个路径,如果有就导入 如果循环没有,会根据header search paths去继续按上面步骤寻找。
预编译完在xcode中找到编译命令:
可以看到编译命令使用了hmap,挂载hmap有两种方式:
-I:挂载system header和user header 都可以,习惯叫system
-iquote:挂载user header
把整个编译命令复制到终端执行,打开产物目录,可以看到编译结果,如果把hmap相关命令删除,编译就会报错,因为找不到对应头文件。需要我们写HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj",上面说了是循环搜索,所以我们要把搜索头文件路径写上去。重新编译,复制编译命令,删除hmap相关参数,在终端执行就会执行成功,此时看导出文件是以模块方式导入。证明了我们上面说的流程。
Realm
我们看下Realm是怎么写的。在podspec文件中s.module_map = 'Realm/Realm.modulemap'设置modulemap,找到对应文件。
可以看到,里面有explicit,这个参数的含义是显式定义子模块。为什么用通配符了还要用这个方式定义子模块,因为有的头文件可能没有放到散头文件中,导入主模块的时候并不会把私有模块引入进去。
Swift Module
创建一个Swift的Static Library工程,修改里面代码进行编译:
import Foundation
@objc
open class SwitfLibraryModule: NSObject {
}
如果我们要把这个静态库提供给别人使用,首先要有SwitfLibraryModule-Swift文件,这个在中间产物目录。在Build->Products里面看到一个.a库和swiftmodule文件,我们也需要把swiftmodule文件提供出去。
现在我们想在OC中用module方式访问swiftLibrary,其实就是以module方式访问-Swift.h文件。
创建SwitfLibraryModule.modulemap文件:
module SwiftLibrary.Swift {
header "SwitfLibraryModule-Swift.h"
requires objc
}
requires指定编译的语言,就是OC可以使用Swift不能使用。
在config中挂载modulemap:-fmodule-map-file="${SRCROOT}/Product/SwitfLibraryModule.modulemap"
在ViewController中导入@import SwiftLibrary.Swift;进行编译,发现报错说找不到这个module。module定义中的.是给主module定义子module。这种写法是定义子模块,但是主模块还没定义,所以我们添加代码:
module SwiftLibrary {
}
所以现在定义子module就有两种方式了。为什么这次我们放在外面定义子module?-Swift.h是系统自动生成的。比如framework是自带module的,写在外面就方便系统给加上swift module,系统会自动把子module放到framework的modulemap里面。
现在再进行编译就会编译成功了。
那现在再创建一个swift文件,导入import SwiftLibrary,会发现报错,因为swift头文件是通过swiftmodule来使用的。所以要告诉sweift编译器去哪个地方找swiftmodule,SWIFT_INCLUDE_PATHS就是这个参数,在config中设置这个环境变量:SWIFT_INCLUDE_PATHS = "${SRCROOT}/Product"。再进行编译就会成功。
这里还有个注意点,就是我们写swift module文件的时候,主module的名字要和module文件名字一致,否则OC引用module没问题,swift引用module就会报错~!
我们在swift文件中定义一个OC的类:
@objc
class Brid: SwitfLibraryModule {
}
进行编译,发现链接时候报错。因为没有把库文件真身导入进来,把.a库直接拖到Frameworks里面:
再进行编译就可以成功了。
如果是swift库提供给OC使用,环境变量要声明OTHER_SWIFT_FLAGS = -Xclang -fmodule-map-file="${SRCROOT}/pchTest/sj/module.modulemap"。
hmap
hmap实际上是hashmap,通过key找到头文件的完整路径。
hmap文件怎么查看,推荐工具:DunmHeaderMap和WriteHeaderMap,下载链接:[hmap]github.com/Cat1237/hma…
我们找一个hmap文件测试一下:
通过json文件生成hmap,创建一个json文件,里面代码:
{
"sj_1.h": [
"sj/",
"sj_1.h"
]
}
安装mapfile,在终端中执行:gem install cocoapods-mapfile,cd到json文件目录,生成hmap文件:hmapfile writer --json-path=./header.json。
可以看到里面都是以key-value的形式存储头文件路径。
我们可以通过hmap进行module映射,来优化编译。hmap是由xcode生成,这个过程是耗时的,我们可以把这个过程节省下来,优化编译。
下面通过hmap来让头文件通过module方式导入:
- 在
config中挂载我们生成的hmap:HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj/header.hmap"。预编译看下编译命令,挂载上了我们的hmap,但是预编译并不是以module方式导入
和hmap相关编译命令如下:
-iquote /Users/shangjie/Library/Developer/Xcode/DerivedData/pchTest-dwkyfwjqgxhryaghltjrurbtppdk/Build/Intermediates.noindex/pchTest.build/Debug-iphonesimulator/pchTest.build/pchTest-generated-files.hmap
-I/Users/shangjie/Library/Developer/Xcode/DerivedData/pchTest-dwkyfwjqgxhryaghltjrurbtppdk/Build/Intermediates.noindex/pchTest.build/Debug-iphonesimulator/pchTest.build/pchTest-own-target-headers.hmap
-I/Users/shangjie/Library/Developer/Xcode/DerivedData/pchTest-dwkyfwjqgxhryaghltjrurbtppdk/Build/Intermediates.noindex/pchTest.build/Debug-iphonesimulator/pchTest.build/pchTest-all-target-headers.hmap
-iquote /Users/shangjie/Library/Developer/Xcode/DerivedData/pchTest-dwkyfwjqgxhryaghltjrurbtppdk/Build/Intermediates.noindex/pchTest.build/Debug-iphonesimulator/pchTest.build/pchTest-project-headers.hmap
-I/Users/shangjie/Library/Developer/Xcode/DerivedData/pchTest-dwkyfwjqgxhryaghltjrurbtppdk/Build/Products/Debug-iphonesimulator/include
-I/Users/shangjie/Desktop/工程化/代码/pchTest_hmaptest/pchTest/sj/header.hmap
pchTest-generated-files.hmap 永远都是空的,暂时没用到
pchTest-own-target-headers.hmap 当前编译target所包含的头文件,这里面只能包含public和private的头文件
pchTest-all-target-headers.hmap 当前project包含所有target头文件,也是只有public和private的头文件
pchTest-project-headers.hmap 真正所有的project头文件
我们可以发现两个hmap都把sj_1.h映射成pchTest/sj_1.h,那生效的是哪个呢,生效的只有一个,就是上面的,编译是一个命令一个命令执行,谁在上面就用谁的。
- 关闭
hmap:USE_HEADERMAP = NO编译还是报错,因为我们hmap只是把头文件sj_1.h==>sj/sj_1.h,并没有映射到头文件的完整路径,系统是再通过一个hmap映射到完整路径。 - 再创建一个
json文件:
{
"sj/sj_1.h": [
"/Users/shangjie/Desktop/工程化/代码/pchTest_hmaptest/pchTest/sj/",
"sj_1.h"
]
}
生成hmap,在config挂载新的hmap:HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj/header.hmap" "${SRCROOT}/pchTest/sj/header_1.hmap",再编译就会成功了。
如果我这么写编译成功:
HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj/header_1.hmap"
USER_HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj/header.hmap"
这么写编译失败:
HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj/header.hmap"
USER_HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj/header_1.hmap"
说明编译器传参-iquote在前,-I在后。
- 使用
module方式导入,就需要modulemap文件,设置HEADER_SEARCH_PATHS = "${SRCROOT}/pchTest/sj" "${SRCROOT}/pchTest/sj/header.hmap"即可!
vfs
vfs:虚拟文件系统,主要是给framework使用。
编译.m代码时候,会先去当前目录找头文件,但是App有个奇怪的点,我们把一个头文件设置成public,默认是project的。
产物会多出来头文件目录:
产物目录中会有一个all-product-headers.yaml的文件就是vfs文件,他做的虚拟文件映射,里面内容:
这个就是把包里面头文件的路径映射到工程里头文件路径,防止你导入的时候会重复定义。
如果把hmap关闭,那vfs文件就不会生成,需要我们自己手动生成。