前言:
编译阶段的优化除了组件二进制化可以实现提前编译 .O文件外,还有没有更进一步的优化方案呢?
首先看下 组件二进制化 = 二进制 +.h ,可以看到除了.a文件外还有.h文件参与编译,所以我们可以从.h编译的角度来考虑进一步的优化,而行之有效的方法就是利用Header Map 技术 。
首先了解下Header Map 是什么
我们知道Xcode是通过Header Search Path来查找引入到项目的头文件的,
#import AFNetworking.h
xcode 编译时读取到目录时拼接上头文件名称
Header Search Path-> AFNetworking.h 文件目录+名称 => 路径
#import <AFNetworking/AFNetworking.h>
其中前半部分 AFNetworking/ 对于动态framework中它代表一个模块也就是module,它的作
用是用来映射头文件的,通过 umbrella header 可以递归导出子目录下的所有.h
而.a静态库也可以这样写,他代表的是真实目录
"${PODS_ROOT}/Headers/Public" #import <AFNetworking/AFNetworking.h>
"${PODS_ROOT}/Headers/Public/AFNetworking" #import AFNetworking.h
我们已知在编译过程中header是参与编译的,它的首次编译过程
1. 根据目录查找头文件(n个组件目录下去找某个头文件)
2. clang 编译header 成二进制 -> .pcm
而通过目录的方式查找头文件,如cocoapods自动生成的debug.xcconfig文件,当项目中很多的文件就会有很多的Header Search Path目录,若在多个目录里面寻找一个头文件,明显会比较耗时,类越多的话查找时间就会越长,所以如果能把一个文件路径提前放在一个文件里面,直接去读取文件里面的路径的话应该会快很多,而Xcode本身就存在Use Header Map配置优化。
所以第一步头文件查找阶段中可以通过开启Xcode配置来进行优化,只需要在 Build Setting 中开启 Use Header Map 选项,就会为我们的主工程以及每个pod组件生成单独hmap文件(Xcode 会为 Pod 生成 5 个 hmap 文件),编译时Clang 就会在 hmap 文件里塞入了一份头文件名和头文件路径的映射表。(开启后可以查看.m文件的编译,发现在编译期底层通过-I参数链接了该组件下的hmap文件),
当再次编译时就会直接使用这些hmap快速找到对应的头文件,编译就会快很多了,不过对Xcode进行清理后这些也都会被删掉。
所以HMap就是系统用来提高头文件查找速度的,通过Header Search Path的设置来查找每个hmap文件,Header Search Path 其底层是-I, 接收两个参数,头文件的目录,指定hmap文件,所以会看到Header Search Path设置了工程以及组件的路径。而xcode通过这个包裹着头文件所在的目录的hmap去查找文件的时候会更快速。但若头文件比较少,hmap的作用越不明显所以不需要用到hmap文件,反而头文件数量越多效果会越明显。
下面是编译时生成的hmap,点开后可以查看文件所在具体路径
hmap文件如何生成?底层结构是什么样的?
首先我们探究下Header Map的底层结构
提前先说明下它其实是一组头文件信息映射表,它是以键值对的形式存在,Key 值是头文件的名称,Value 是头文件的实际物理路径(-I可以通过hmap解析到头文件所在的目录)
如果我们通过cat命令查看.hmap中的内容是下面这样的,前半部分是配置文件,后半部分存储真正的信息
那hmap其中的结构是什么样子的?
关于Hmap在LLVM的源码中有相关详细的介绍,Tests target中存在两个结构体HMapBucket和HMapHeader,并有将Hmap解析成HMapBucket和HMapHeader的源代码,以及如何生成hmap的源码。
Hmap的结构
可以看到hmap文件由HampHeader和HmapBucket以及一个string_table组成.
最上层是Hmapheader,保存了当前Hmap的详细信息,可以通过读取Hmapheader,知道当前二进制里面有多少个Bucket。
HmapBucket:保存了Hmap的具体信息,有Key、Prefix、Suffix三个字段代表了在string_table偏移量,可以根据偏移量计算出保存字符串的位置。
Key 代表具体头文件 --AFNetworking.h
Prefix 代表头文件的前半部分(目录),Suffix表示头文件的后半部分(头文件名称),也就是Prefix + Suffix 代表一个头文件的完整路径。
比如target中包含3个头文件,则会有3个HmapBucket,Bucket的数量与头文件的数量相等,
Bucket中的偏移量,可以根据偏移量计算出保存字符串的位置,从而读取出头文件的路径。
string_table:头文件字符串
Bucket首地址: string_table首地址 = HMapHeader大小 + num *HMapBucket
具体如何读这里不再介绍,可以直接用开源工具DumpHeaderMap、hmap 工具来将文件读取成结构体,再从中读取出头文件的字符串。
解析出来的Hmap是下面这个样子:
Hash bucket count: 4
String table entry count: 4
Max value length: 0 bytes
- Bucket 0: Key TestApp-Bridging-Header.h -> Prefix /Users/cloud/Documents/iOS/TestApp/TestApp/, Suffix TestApp-Bridging-Header.h
- Bucket 1: Key ViewController.h -> Prefix /Users/cloud/Documents/iOS/TestApp/, Suffix ViewController.h
- Bucket 2: Key AppDelegate.h -> Prefix /Users/cloud/Documents/iOS/TestApp/, Suffix AppDelegate.h
- Bucket 3: Key SceneDelegate.h -> Prefix /Users/cloud/Documents/iOS/TestApp/, Suffix SceneDelegate.h
如何生成Hmap
在LLVM的源码中的HeaderMapTest.cpp 中有关于生成Hmap详细介绍,大概步骤如下
创建MapFile容器Maker,Maker包含一个个的MapFile,也就是Bucket,其中8代表有八个Bucket,750代表大小
生成一个Bucket,然后将类名和路径以Bucket的形式保存
将文件导出到指定位置
最后重写 xcconfig 文件里的 Header Search Path 到对应的 hmap 文件上。
那是不是我们只要开启 Xcode 提供的 Use Header Map 就可以提升编译速度了呢?
在Clang生成Hmap内容时,键值内容会随着使用场景产生不同的变化,例如头文件引用是在 "..." 的形式下,还是 <...> 的形式下,又或是在 Build Phase 里 Header 的配置情况。又或者你将头文件设置为 Public 的时候,在某些 hmap 中,它的 Key 值就为 PodA/ClassA,而将其设置为 project 的时候,它的 Key 值可能就是 ClassA,
而在基于 CocoaPods 管理项目的状况下,由于Xcode 和 CocoaPods 在工程和头文件上的的管理上理念的不同会给我们带来新的问题。
当我们构建的产物类型为 Static Library 的时候,CocoaPods 在创建头文件产物过程中,它的逻辑大致如下:
1:不论 podspec 里如何设置 public_header_files 和 private_header_files,相应的头文件都会被设置为 Project 类型。
2:如果 podspec 里未标注 Public 和 Private 的时候,Pods/Headers/Public 和 Pods/Headers/Private 的内容一样且会包含所有头文件。
这里的主要问题是1:
就是在 Static Library 的状况下,一旦我们开启了 Use Header Map,结合组件里所有头文件的类型为 Project 的情况,这个 hmap 里只会包含 #import "ClassA.h" 的键值引用,也就是说只有 #import "ClassA.h" 的方式才会命中 hmap 的策略,否则都将通过 Header Search Path 寻找其相关路径,这样开启 Use Header Map 并不会提升编译速度 (使用 Framework 的情况就没问题)
我们知道在引用其他组件的时候,通常都会采用 #import <A/A.h> 的方式引入。至于为什么会用这种方式,一方面是这种写法会明确头文件的由来,避免问题,另一方面也是这种方式可以让我们在是否开启 clang module 中随意切换。当然,还有一点就是Apple 在 WWDC 里曾经不止一次建议开发者使用这种方式来引入头文件。
所以说在引入组件比较多的情况下势必导致编译速度过慢了。
美团方案
美团在今年初发布了他们以 Header Map 技术 为基础自研出的一款 cocoapods 插件cocoapods-hmap-prebuilt,来进一步提升代码的编译速度,完善头文件的搜索机制。
据说cocoapods-hmap-prebuilt 插件能将总链路提升 45% 以上的速度,在 Xcode 打包环节上能提升 50% 以上的速度,当然这个效率的提升是建立在美团几百+数量庞大的组件基础上的,hmap也只对组件化项目效果明显。
大概看了文章介绍理解下来感觉是写了一个脚本去遍历项目以及三方库的头文件,将所有头文件提取出来后,用上面的方法生成一个hmap,其中重点是做了组件名/头文件名方式的 Key-Value 的优化,重写 xcconfig 文件里的 Header Search Path 到对应的 hmap 文件上,并将项目中组件自身的 Ues Header Map 功能关闭,减少了不必要的文件创建和读取。
以上就是所有关于Hmap的内容啦,希望有帮助到大家进一步了解头文件的编译优化!