一次Clang Module和hmap的实践记录

2,406 阅读6分钟

前言

年前看了 从预编译的角度理解Swift与Objective-C及混编机制以及 Swift 与 Objective-C 混编时,我们是如何将编译时间优化了 35%?,动手实践了一下,记录一下实践过程中的一些问题。

回顾

Clang Module & hmap.png 根据两篇文章以及我自己的理解简单梳理了一下。

目标

1.对项目进行module化
2.利用hmap进行编译速度优化

代码

代码地址

6F841C0A-FC50-4796-908D-54AA65D2EB79.png
使用了本地ruby库,请确保有安装bundler

module化

项目的module化比较简单,首先开启module功能。
文中提到了,可以在Podfile 中添加 use_modular_headers! 对所有的 pod 进行 Module 化; 并修改每个pod对应的 .podspec 中的 xcconfig 中的配置 DEFINES_MODULE,如 s.xcconfig = {'DEFINES_MODULE' => 'YES'}。
这种方式需要修改每个pod的podspec文件,略显麻烦。

我这里使用的是 use_frameworks! :linkage => :static
把pod库打成framework会自动打开module功能。

之后就是对pod库以及主工程进行module化适配,我遇到的基本上都是头文件引入不规范和Masonry的宏问题,都很容易解决,不复赘述。

这里是我在项目中的编译时间测试。

开关第1次第2次第3次第4次
打开Module762s661s633s632s
关闭Module586s498s419s552s

本机上编译,数据起伏比较大,但是也能看出开启Module后,确实会影响编译速度。

系统生成hmap的问题

我做了一些测试,为了查看一下系统生成的hmap在编译时的命中情况。
2DE0909D-F687-4A37-9E59-E6A1255D8D37.png

测试工程结构大致如图。
MyMainAPP依赖MyPodC,MyPodC依赖SDWebImage。
在MyMainAPP的ViewController中引入
#import "MyPodCView.h"
#import <MyPodC/MyPodCView.h>
在MyPodC的MyPodCView中引入
#import "SDImageCache.h"
#import <SDWebImage/SDImageCache.h>

观察ViewController.m和MyPodCView.m在编译的Header Search Path

当MyMainAPP以.a静态库的方式引入MyPodC时

编译ViewController.m

a-main.png

a-main-map.png

a-main-点.png 总结如下:
#include "..." search starts here:
MyMainAPP-generated-files.hmap    空
MyMainAPP-project-headers.hmap   只有自己的,未命中
#include <...> search starts here:
MyMainAPP-own-target-headers.hmap  空
MyMainAPP-all-target-headers.hmap  空

编译MyPodCView.m

a-pod.png

a-pod-map.png

a-pod-点.png

a-pod-点2.png

总结如下:
#include "..." search starts here:
MyPodC-generated-files.hmap   空
MyPodC-project-headers.hmap   只有自己的,未命中
#include <...> search starts here:
MyPodC-own-target-headers.hmap 只有自己的,未命中
MyPodC-all-target-headers.hmap 空

当MyMainAPP以framework静态库的方式引入MyPodC时

编译ViewController.m

f-main.png

a-main-map.png

f-main-点.png

总结如下:
#include "..." search starts here:
MyMainAPP-generated-files.hmap  空
MyMainAPP-project-headers.hmap 只有自己的,未命中
#include <...> search starts here:
MyMainAPP-own-target-headers.hmap 空
MyMainAPP-all-non-framework-target-headers.hmap 空

编译MyPodCView.m

f-pod.png f-pod-map.png f-pod-点.png f-pod-jian.png 总结如下:
#include "..." search starts here:
MyPodC-generated-files.hmap 空
MyPodC-project-headers.hmap  只有自己的,未命中
#include <...> search starts here:
MyPodC-own-target-headers.hmap 只有自己的,未命中
MyPodC-all-non-framework-target-headers.hmap 空

这是我在开启 install! 'cocoapods', generate_multiple_pod_projects: true的一个测试结果。开启generate_multiple_pod_projects后,每个Pod会单独生成project。如果关闭这个开关,生成hmap也会有所不同。
但是基本上,可以看出Xcode生成的hmap没有起到作用,基本没有命中。

自己生成hmap进行优化

《 Swift 与 Objective-C 混编时,我们是如何将编译时间优化了 35%》 一文中详细描述了hmap插件的大致功能。避免重复造轮子,还是先看看有没有现成的。

65BDBAD8-6923-499B-92DF-1A433166DF20.png
大致看了一下几个库,发现cocoapods-hmap-prebuilt库比较符合。
插件原理很简单,就是hook pod install/update 操作,给每个pod库生成一个hmap。之后修改pod库和主工程的xcconfig中的Header Search Path,只保留hmap的路径。

这里引用一下文中的原理:

  1. HooksManager注册cocoapods的post_install钩子;
  2. 通过header_mappings_by_file_accessor遍历所有头文件和header_dir,由header_dir/header.h和header.h为key,以头文件搜索路径为value,组装成一个Hash<key,value>,生成所有组件pod头文件的json文件,再通过hmap工具将json文件转成hmap文件。
  3. 再修改各pod中.xcconfig文件的HEADER_SEARCH_PATHS值,仅指向生成的hmap文件,删除原来添加的搜索目录;
  4. 修改各pod的USE_HEADERMAP值,关闭对默认的hmap文件的访问。

插件修改过后:
MyMainAPP的xcconfig如下: HEADER_SEARCH_PATHS = ${PODS_ROOT}/Headers/HMap/Pods-MyMainAPP-prebuilt.hmap
某个Pod库MyPodA的xcconfig如下:
HEADER_SEARCH_PATHS = ${PODS_ROOT}/Headers/HMap/MyPodA-prebuilt.hmap ${PODS_ROOT}/Headers/HMap/Pods-MyMainAPP-prebuilt.hmap

Pods-MyMainAPP-prebuilt.hmap中包含了所有pod库的"View.h"、"<Pod/View.h>"的路径键值对。 MyPodA-prebuilt.hmap中包含了本库中类似"MyPodA.h"、"<MyPodA/MyPodAView.h>"这样的路径键值对。

我稍微做了几处改动:
1.hmap工具路径。
使用该插件需要先安装hmap工具,但是我一直没有安装成功,所以使用了本地路径。

F9DD299C-B549-4036-9484-CF21C1711EC8.png

2.有些三方库名称与模块名不一致。

28FDFABF-7337-4144-9FAF-4AE47E194386.png

3.删除PODS_CONFIGURATION_BUILD_DIR路径

3C4F20F1-7E0B-4245-9173-C4A4C21CDBCA.png
PODS_CONFIGURATION_BUILD_DIR路径开头的一般都是Pod库framework的路径,已经在hmap中命中了。而系统头文件路径是在Header Search Path最下面的,删除PODS_CONFIGURATION_BUILD_DIR路径可以让系统头文件搜索更快一些。

测试结果

这是一份在我项目中的测试结果
578s 不开clang module
511s  不开 clang module  + hmap
673s 开 clang module
602s 开 clang module  + hmap
522s 开 clang module  + hmap + 去除残余PODS_CONFIGURATION_BUILD_DIR

数据非常不稳定,大致符合预期。
什么样的预期? 开启clang module会减慢编译速度,使用hmap可以优化头文件搜索速度,进而优化编译速度。

这样就结束了吗?

这个时候,我陷入了一个认识误区。我一直以为clang module和 hmap是兼容的,也就是说,开启clang module后,对pod库头文件的引入会从单纯的头文件内容拷贝变为module式引入,而hmap优化了module引入时搜索头文件的速度。

这个认识是错误的❌。

主工程MyMainAPP依赖了Pod库SDWebImage。已开启clang module功能,已生成hmap

15DCD756-6BC1-40D8-A811-171457CD98A6.png

并在ViewController中引入。根据clang module的功能说明, 编译器理应
自动将#import <SDWebImage/SDAnimatedImagePlayer.h>变为@import SDWebImage.SDAnimatedImagePlayer;

24260FA9-1599-4DEB-A089-E2C4341057B9.png

但实际上,查看预编译结果,却又变回拷贝式的引入 (注意项目需要clean,并清理module的pcm文件缓存目录)

F9B4B183-B4F8-4481-BE34-FC7683BF6393.png

手动改为@import SDWebImage.SDAnimatedImagePlayer;,却编译报错了。

8AFAB267-79C6-4E40-8E0E-68EA3A7E97DD.png

2C9D250A-A3C6-4916-A9E9-C6A94A7EB890.png

看一下编译ViewController.m时的Header Search Path

6D2E19B9-A7BB-4005-B2B1-9CB6C4342477.png

打印一下hmap

9C81059C-3B2E-472A-9960-3B92307056F6.png

回忆了一下Clang Module编译流程,我是这样理解的:

当编译器在编译ViewController.m时,遇到#import <SDWebImage/SDAnimatedImagePlayer.h>,就会去查询Header Search Path,结果第一个路径就命中了hmap,而hmap告诉编译器头文件在我的项目工程目录下(MyMainAPP/Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.h),这时候他会去Modules 目录下查找 modulemap 文件。结果没找到,未命中module 缓存,所以编译器决定用拷贝方式引入。

这个时候,删除Header Search Path中的hmap路径,clang module又能正常编译通过了。

C9217DC2-250F-4B7E-98D0-CB1218815B8C.png

这是因为编译时命中了Header Search Path中build目录下SDWebImage.framework目录。

之后,我有两个思考方向:
1.把modulemap文件和umbrella.h文件放到MyMainAPP/Pods/SDWebImage/SDWebImage/Core/路径下,编译器是否就能找到了呢。尝试过后不可行。
2.如果我能生成编译后的路径到hmap里,不就可以解决了吗。比如Xcode/DerivedData/MyMainAPP-hapsxqbzqwmfppcbsypzshyvkwtw/Build/Products/Debug-iphoneos/SDWebImage/SDWebImage.framework/Headers/SDAnimatedImagePlayer.h。 但是环境变量PODS_CONFIGURATION_BUILD_DIR需要在编译开始时才能拿到。那就需要给每个Target的Build Phases插入一个脚本,拿到编译后的目录,再生成hmap。

但是,大佬们的文章里没有提到需要这样生成hmap。思来想去没有好的解决办法,直到我反复回去看文章,看到这样一句话

由于 Swift/OC 混编下需要 Module 化的支持,同时借鉴业内 HeaderMap 方案让 OC 调用 OC 时避开 Module 化调用

什么?避开? 原来是我自己想岔了。

最后

Clang Module可以让OC和Swift互相调用更加方便、优雅,但是会引起编译时间变长。
使用hmap,可以让头文件引入又变回文本引入。但是hash查找头文件路径也加速了编译。

个人技术水平有限,都是自己瞎琢磨的,如有错误,请指正。

最后,感恩大佬们的技术文章。