iOS - header map加快编译速度(一)

1,290 阅读6分钟

hmap - header map 描述头文件映射关系

.m文件 -> 最终app 的简单过程主要是两部分

  • .m -> .o 编译生成.o文件,静态库在此阶段生成
  • .o -> app 链接生成app (link)

这两个阶段都需要时间,要考虑如何加快编译速度,首先得选择在以上哪个阶段处理,选择的标准就是看哪个阶段的耗时更长,处理才变得更有意义

image.png

编译消耗时间 167毫秒,链接消耗时间

时间差一个数量级,所以处理编译阶段时间优化更有效

如果头文件目录很多,意味着耗时进一步增加

  • 针对于编译.o 优化的几种方式

    • 组件二进制化 提前编译二进制库文件(打包静态库 dylibs framework)

      这种方式可以参考另一篇博文 iOS解耦合-你做到了吗? 了解

    • header 映射

      在理解header映射之前,可以试想一下,你有没有遇到过项目里包含很多资源文件的情况,我曾经的一个项目里需要用到的音频文件有超过一万个,而编译单索引这些资源文件就相当耗时,单单这些资源文件的编译当时至少4分钟

      header可以理解为一种资源文件,我们能处理的就是搜索资源过程做优化

header编译?

header是否参与编译

简单设想一下,在.h中定义个函数,在.m中调用定义的函数,是否执行,若执行的话,如果.h不编译,函数又从何而来呢

根据矛盾反向分析,.h肯定是参与编译的

如果有研究过oc底层源码的话, .h中有很多定义, 可以看下 objc_class .h中定义

image.png

.h如何编译进来 (组件二进制 = 二进制 + .h)

.h存在于何处 如何查找

  • xcode - header search paths -> 查找目录
  • import xxx.h

最终 目录/xxx.h 找到头文件

framework的头文件引入方式 <xxxxFramework/xxxx.h>, 也就是module/头文件

framework -> module -> 映射头文件

比如 pod安装 Masonry, Masonry.framework 包内容,存在 Modules/module.modulemap 这样一个文件

framework module Masonry {
  umbrella header "Masonry-umbrella.h"

  export *
  module * { export * }
}

umbrella 指向一个头文件 Masonry-umbrella.h

umbrella header Masonry-umbrella.h 就是 module的伞柄

系统通过 module 抓伞柄,找伞骨,每个伞骨映射一个头文件

image.png

image.png

.a 头文件引入 - 目录/xxx.h

看下pod生成的静态库

注释掉 Podfile use_frameworks!

image.png

重新 pod install

cocoapods 生成xcconfig

配置了 header search paths

image.png

既然配置了header 查找目录

比如配置了 ${PODS_ROOT}/Headers/Public

就可以通过 Public目录下 AFNetworking/xxx.h的方式查找头文件了

build经验

在你编译项目的时候,是否有这样的经验

首次编译的时候特别慢 第二次变快了

根据cocoapods 项目,其实就是一种组件二进制化的方式

既然已经二进制化了,为什么首次会慢了呢

当你在代码里引入 .h文件时,就需要到目录里去查找,意味着需要在很多的头文件里找到目标文件

这其实是一个耗时的过程 所以会觉得慢

首次编译时,会把.h编译成二进制形式 ,.h编译的二进制存放在什么地方

image.png

以下就是头文件编译之后的二进制形式了

image.png

image.png

再次编译的时候,直接读取.h编译的二进制文件,节省了目录搜索的过程,效率会高一些

如何找到 这些 二进制文件呢?

引出hmap

首次编译之后,xcode会生成.hmap文件

image.png

再次编译时,就是通过这些 hmap文件 找到对应的 .h编译的二进制文件

可以看到好几个.hmap文件,其实是不同类别的文件

清理一次工程,这些.hmap也会消失,编译又会变慢

hmap 字面理解就是 header映射,里面至少是 key - value这样的映射结构

简单看下,xcode在编译 一个.m文件时,-I 引入了.hmap文件

image.png

cat命令查看下 主工程项目的hmap文件 - IFLTestSymbol-project-headers.hmap

image.png

图中能看到大概信息包含3个.h文件,一个静态库.a头文件,两个主工程中的.h文件

hmap数据结构

llvm源码中查看hmap数据结构

image.png

仔细理解下HMapHeader 结构体最后的两句注释

image.png

  • HMapHeader 包含头部信息,就是 Magic, Version, Reserved 等信息
  • HMapBucket数组区域, 数量:NumBuckets个
  • 一长串字符串

其中 HMapBucket结构中的 Key,Prefix,Suffix并不是字符串,而是各自代表的字符串在 长字符串中的偏移量

可以这样理解,hmap 的关键信息 key:目录前缀/头文件

读取到key的偏移,上 字符串中 根据key的偏移取出 key字符串

读取到前缀偏移,上 字符串中 根据前缀的偏移取出 前缀字符串

读取到后缀偏移,上 字符串中 根据后缀的偏移取出 后缀字符串

根据 key字符串,拿出 目录前缀/头文件

c++读hmap

稍微调整下HMapHeader结构

image.png

void read_hmap(void) {
    // test_hmap/Test111-all-non-framework-target-headers.hmap
    // test_hmap/Test111-all-target-headers.hmap
    // test_hmap/Test111-own-target-headers.hmap
    // test_hmap/Test111-project-headers.hmap
    // test_hmap/IFLTestSymbol-all-target-headers.hmap
    // test_hmap/IFLTestSymbol-generated-files.hmap
    // test_hmap/IFLTestSymbol-own-target-headers.hmap
    // test_hmap/IFLTestSymbol-project-headers.hmap
//    char *path = "/Users/erlich/Developer/workspace/ios/test/test_symbol/Test111/HMap/Test111.build/Debug-macosx/Test111.build/Test111-project-headers.hmap";
    char *path = "/Users/erlich/Developer/workspace/ios/test/test_symbol/Test111/Test111/test_hmap/Test111-project-headers.hmap";
    int file = open(path, O_RDONLY|O_CLOEXEC);
    if (file < 0) {
        printf("cannot open file %s", path);
        return;
    }
    struct HMapHeader *header = malloc(100 * sizeof(struct HMapHeader));
    ssize_t headerRead = read(file, header, 100 * sizeof(struct HMapHeader));
    if (headerRead < 0 || (size_t)headerRead < sizeof(struct HMapHeader)) {
        printf("read %s fail", path);
        close(file);
        return;
    }
    close(file);
    
    // Sniff it to see if it's a headermap by checking the magic number and version.
    bool needsByteSwap = false;
    if (header->Magic == ByteSwap_32(HMAP_HeaderMagicNumber) && header->Version == ByteSwap_32(HMAP_HeaderVersion)) {
        // 高低位变换
        needsByteSwap = true;
    }
    
    uint32_t NumBuckets = needsByteSwap ? ByteSwap_32(header->NumBuckets) : header->NumBuckets;
    uint32_t StringsOffset = needsByteSwap ? ByteSwap_32(header->StringsOffset) : header->StringsOffset;
    
    const void *raw = (const void *)header;
    
    // HMapBucket 数组
    const void *buckets = raw + 24;
    // 长字符串
    // const void *string_table = raw + 24 + 8 + header->StringsOffset;
    // 理解错误,修正,原来 HMapHeader结构中的 StringsOffset 指的是 从结构体起始位置偏移,非buckets开始的偏移
    const void *string_table = raw + header->StringsOffset;

    printf("buckets 初始化了: %i\n\n", NumBuckets);
//    printf("长字符串:%s\n\n", string_table);
    
    int mBucketsCount = 0;
    for (uint32_t i = 0; i < NumBuckets; i++) {
        struct HMapBucket *bucket = (struct HMapBucket *)(buckets + i * sizeof(struct HMapBucket));
        bucket->Key = needsByteSwap ? ByteSwap_32(bucket->Key) : bucket->Key;
        bucket->Prefix = needsByteSwap ? ByteSwap_32(bucket->Prefix) : bucket->Prefix;
        bucket->Suffix = needsByteSwap ? ByteSwap_32(bucket->Suffix) : bucket->Suffix;
        
        if (bucket->Key == 0 && bucket->Prefix == 0 && bucket->Suffix == 0) {
            continue;
        }
        mBucketsCount++;
        const char *key = string_table + bucket->Key;
        const char *prefix = string_table + bucket->Prefix;
        const char *suffix = string_table + bucket->Suffix;

        printf("key: %s, offset: %i \nprefix: %s, offset: %i, \nsuffix: %s, offset: %i\n\n", key, bucket->Key, prefix, bucket->Prefix, suffix, bucket->Suffix);
    }
    
    printf("buckets 初始化了%i个,实际使用了%i个\n\n", NumBuckets, mBucketsCount);
    
    free(header);
}

image.png

由于读取hmap内容的c++代码并没有 内存偏移处理,所以做了取巧处理

  • 将HMapBucket数组作为 HMapHeader的成员
  • 长字符串具体长度目前未知,读取hmap缓冲大小设定了一定的冗余空间
  • header结构体内容 参考llvm源码中的逻辑,需要做高低位反转判断
  • 长字符串部分初始需要做偏移 - header->StringsOffset

获取到的key prefix suffix 与预期的有一定的偏差

读取环节有纰漏,大概可以推断出错的缘由在于字符串偏移上,明显是偏移过多了,也就是偏移起始出错了

  • 修正,原来 HMapHeader结构中的 StringsOffset 指的是 从结构体起始位置偏移,非buckets开始的偏移

image.png

读取hmap内容就显示正常了

image.png

hmap理解

理解pod xcconfig header search path

image.png

有两种头文件引入方式

  • #import AFNetworking.h

    .../Headers/Public/AFNetworking 这种配置 目录 会跟 AFNetworking.h拼接

  • #import <AFNetworking/AFNetworking.h>

    .../Headers/Public 这种配置目录 会跟 AFNetworking/AFNetworking.h 拼接

剩下的就是hmap配置了

当前配置是根据 header search paths 最终找到 前缀目录+后缀头文件.h

现在换成 不查找目录,直接查找hmap中的 key,拿到 prefix + suffix拼接的结果

具体配置及插件,下一篇博文会更新此内容