前言
年前看了 从预编译的角度理解Swift与Objective-C及混编机制以及 Swift 与 Objective-C 混编时,我们是如何将编译时间优化了 35%?,动手实践了一下,记录一下实践过程中的一些问题。
回顾
根据两篇文章以及我自己的理解简单梳理了一下。
目标
1.对项目进行module化
2.利用hmap进行编译速度优化
代码
使用了本地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次 |
|---|---|---|---|---|
| 打开Module | 762s | 661s | 633s | 632s |
| 关闭Module | 586s | 498s | 419s | 552s |
本机上编译,数据起伏比较大,但是也能看出开启Module后,确实会影响编译速度。
系统生成hmap的问题
我做了一些测试,为了查看一下系统生成的hmap在编译时的命中情况。
测试工程结构大致如图。
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
总结如下:
#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
总结如下:
#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
总结如下:
#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
总结如下:
#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插件的大致功能。避免重复造轮子,还是先看看有没有现成的。
大致看了一下几个库,发现cocoapods-hmap-prebuilt库比较符合。
插件原理很简单,就是hook pod install/update 操作,给每个pod库生成一个hmap。之后修改pod库和主工程的xcconfig中的Header Search Path,只保留hmap的路径。
这里引用一下文中的原理:
- HooksManager注册cocoapods的post_install钩子;
- 通过header_mappings_by_file_accessor遍历所有头文件和header_dir,由header_dir/header.h和header.h为key,以头文件搜索路径为value,组装成一个Hash<key,value>,生成所有组件pod头文件的json文件,再通过hmap工具将json文件转成hmap文件。
- 再修改各pod中.xcconfig文件的HEADER_SEARCH_PATHS值,仅指向生成的hmap文件,删除原来添加的搜索目录;
- 修改各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工具,但是我一直没有安装成功,所以使用了本地路径。
2.有些三方库名称与模块名不一致。
3.删除PODS_CONFIGURATION_BUILD_DIR路径
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
并在ViewController中引入。根据clang module的功能说明, 编译器理应
自动将#import <SDWebImage/SDAnimatedImagePlayer.h>变为@import SDWebImage.SDAnimatedImagePlayer;
但实际上,查看预编译结果,却又变回拷贝式的引入 (注意项目需要clean,并清理module的pcm文件缓存目录)
手动改为@import SDWebImage.SDAnimatedImagePlayer;,却编译报错了。
看一下编译ViewController.m时的Header Search Path
打印一下hmap
回忆了一下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又能正常编译通过了。
这是因为编译时命中了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查找头文件路径也加速了编译。
个人技术水平有限,都是自己瞎琢磨的,如有错误,请指正。
最后,感恩大佬们的技术文章。