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

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

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

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

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

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

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

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

image.png
  • 针对于编译.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的伞柄

系统通过伞柄 去映射到 每个伞骨代表的头文件

image.png
image.png

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

看下pod生成的静态库

注释掉 Podfile use_frameworks!

image.png

重新 pod install

cocoapods 生成xcconfig

image.png

既然配置了header 查找目录

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

就可以通过 Public目录下 AFNetworking/xxx.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;

    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拼接的结果

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容