当前位置: 首页 > 科技观察

什么工具

时间:2023-03-14 20:34:56 科技观察

cocoapods-hmap-prebuilt可以将大型iOS项目的编译速度提高50%?cocoapods-hmap-prebuilt是美团平台迭代组自主研发的cocoapods插件。基于HeaderMap技术,进一步提高了代码编译速度,完善了头文件查找机制。虽然以二进制组件的形式构建App是HPX(美团移动端统一持续集成/交付平台)的主流方案,但在某些场景(Profile、Address/Thread/UB/CoverageSanitizer、App级静态检查、ObjC方法调用)兼容性检查等),我们的构建工作还是需要以全源码编译的方式来完成;而在实际的开发过程中,大部分都是采用源码的方式进行开发,所以我们将实验对象设置为基于全源码的编译过程。话不多说,让我们来看看它的实际使用效果吧!总的来说,在以美团和大众点评为实验对象的全源码编译过程的前提下,cocoapods-hmap-prebuilt插件可以使总链接速度提升45%以上,并且可以增加Xcode打包链接速度提升50%以上的速度你心动了吗?为了更好的理解这个插件的价值和作用,我们不妨看一下当前项目中存在的问题。为什么现有项目不够好?目前美团内的app都是基于CocoaPods进行包管理的,所以在实际开发过程中,CocoaPods会在Pods/Header/目录下添加组件名目录和头文件软链接,类似如下形式:/Users/sketchk/Desktop/MyApp/Pods└──Headers├──Private│└──AFNetworking│├──AFHTTPRequestOperation.h->./XXX/AFHTTPRequestOperation.h│├──AFHTTPRequestOperationManager.h->。/XXX/AFHTTPRequestOperationManager.h│├──...│└──UIRefreshControl+AFNetworking.h->./XXX/UIRefreshControl+AFNetworking.h└──Public└──AFNetworking├──AFHTTPRequestOperation.h->。/XXX/AFHTTPRequestOperation.h├──AFHTTPRequestOperationManager.h->./XXX/AFHTTPRequestOperationManager.h├──...└──UIRefreshControl+AFNetworking.h->./XXX/UIRefreshControl+AFNetworking.h也是通过With这样的目录结构和软链接,CocoaPods可以在HeaderSearchPath中加入如下参数,使得预编译过程顺利进行。$(inherited)${PODS_ROOT}/Headers/Private${PODS_ROOT}/Headers/Private/AFNetworking${PODS_ROOT}/Headers/Public${PODS_ROOT}/Headers/Public/AFNetworking虽然这种构建SearchPath的方式解决了问题的预编译,但是在一些项目中,比如组件高达400+的巨型项目,会造成如下问题:-I${PODS_ROOT}/Headers/Private想要解决以上问题,更好的情况是下一种情况,可能会浪费1个小时,糟糕的情况是让有风险的代码上线。你觉得工程师会因此而头疼吗?什么是标题映射?好在cocoapods-hmap-prebuilt的出现让这些问题成为了历史,但是要想理解它为什么能解决这些问题,我们首先要了解什么是HeaderMap。HeaderMap其实就是一组头文件信息映射表!为了更直观的理解HeaderMap,我们可以在BuildSetting中开启UseHeaderMap选项来实地体验一下。然后在BuildLog中获取相应组件中相应文件的编译命令,在最后添加-v参数可以查看其运行的秘密:$clang-csome-file.m-osome-file.o-v在控制台的输出内容中,我们会发现一段有趣的内容:通过上图,我们可以看到编译器会显示头文件的顺序和对应的路径,而在这些路径中,我们看到一些奇怪的东西,就是后缀为.hmap的文件,后面有一个括号,上面写着headermap。这是正确的!它是HeaderMap的实体。这时候Clang已经在刚才提到的hmap文件中插入了一个头文件名和头文件路径的映射表,不过是二进制格式的文件。为了验证这个说法,我们可以使用milend工具编写的hmap来查看其内容。执行相关命令(hmapprint)后,我们可以发现这些hmaps中存储的信息结构大致如下,类似于一个Key-Value的形式,Key值为头文件名,Value为头文件名头文件的实际物理路径:需要注意的是映射表的key-value内容会根据使用场景的不同而不同。例如,头文件引用的形式是“...”还是<...>,或者在BuildPhase中对Header的配置。比如当你设置头文件为Public时,在某些hmap中,它的Key值为PodA/ClassA,而当你设置为project时,它的Key值可能为ClassA,这些信息所在的配置,如图下图:至此,我想你应该明白什么是HeaderMap了吧。当然,这项技术并不是什么新鲜事物。Facebook的buck工具也提供了类似的东西,只是文件类型变成了HeaderMap.java。这个时候我猜你可能对buck没有太大的兴趣,而是开始思考上图中Headers的Public、Private、Project分别代表什么。好像很多同学都没有怎么关注过,为什么会影响到hmap里面的内容呢?WhatisPublic,Private,Project?在Apple官方的XcodeHelp-Whatarebuildphases?文档中,我们可以看到如下的一段解释:Associatespublic,private,orprojectheaderfileswiththetarget.PublicandprivateheadersdefineAPIintendedforusebyotherclients,andarecopiedintoaproductforinstallation.Forexample,publicandprivateheadersinaframeworktargetarecopiedintoHeadersandPrivateHeaderssubfolderswithinaproduct.ProjectheadersdefineAPIusedandbuiltbyatarget,butnotcopiedintoaproduct.Thisphasecanbeusedoncepertarget.总的来说,我们可以知道的一点是BuildPhases-Headers中提到的Public和Private是指可以被外界使用的头文件,而Project中的头文件是不对外使用的并且不会被放置在最终产品中。如果继续看一些资料,比如StackOverflow-Xcode:CopyHeaders:Publicvs.Privatevs.Project?和StackOverflow-UnderstandingXcode'sCopyHeadersphase,你会发现在早期的XcodeHelp的ProjectEditor章节中,有一段叫做SettingTheRoleofaHeaderFile的段落,里面详细介绍了三种类型的区别。公共:接口已经完成,供您产品的客户使用。公共标头作为可读源代码包含在产品中,不受限制。私有:该界面不适用于您的客户,或者它处于开发的早期阶段。产品中包含一个私有标头,但它被标记为“私有”。因此这些符号对所有客户端都是可见的,但客户端应该明白他们不应该使用它们。项目:该接口仅供当前项目中的实现文件使用。项目标头不包含在目标中,目标代码中除外。客户根本看不到这些符号,只有您可以看到。至此,我们应该可以完全理解Public、Private、Project的区别了。总之,Public还是通常意义上的Public,Private是InProgress的意思,Project是通常意义上的private的意思。至此,你是不是觉得CocoaPods中Podspec的Syntax中有public_header_files和private_header_files这两个字段。它们的实际含义是否与Xcode中的概念冲突?这里仔细阅读官方文档的解释,尤其是private_header_files字段。我们可以看出这里的private_header_files的意思是相对于Public的。这些头文件的本意并不是要暴露给用户,也不会生成相关文件,但是在构建的时候,它会出现在最终的产品中。只有既没有标记为Public也没有标记为Private的头文件才会被认为是真正的私有头文件,不会出现在最终产品中。CocoaPodsforPublicandPrivate的官方解释似乎与Xcode中的描述是一致的。两地的Private并不是我们平时理解的Private。文件,更像是一个InProgress的意思。这个是不是让你有点吃惊?那么,在现实世界中,我们是否正确使用了它们?为什么使用原生hmap不能提高编译速度?前面我们介绍了hmap是什么以及如何启用它(在BuildSetting中启用UseHeaderMap选项),也介绍了一些影响hmap生成的因素(Public、Private、Project)。那是不是说我只要打开Xcode提供的UseHeaderMap就可以提高编译速度呢?很遗憾,答案是否定的!至于原因,我们先从下面的例子说起,假设我们有一个基于CocoaPods的全源工程项目,它的整体结构如下:首先,Host和Pod是我们两个项目的产物,Pods下的Target类型是静态库。其次,Host下会有一个同名的Target,Pods目录下会有n+1个Target,其中n取决于你依赖的组件个数,1是一个名为Pods-XXX的Target,最后,Pods-XXX这个Target的产物会被Host中的Target所依赖。整个结构如下:当构建的product类型为StaticLibrary时,CocoaPods创建头文件product过程中的逻辑大致如下:无论podspec中public_header_files和private_header_files如何设置,对应的头文件将被设置为Project类型。所有声明为public_header_files的头文件都保存在Pods/Headers/Public中。所有的头文件都会保存在Pods/Headers/Private中,不管是public_header_files还是private_header_files描述的,或者没有描述的。该目录是当前组件所有头文件的完整集合。如果podspec中没有标明Public和Private,那么Pods/Headers/Public和Pods/Headers/Private的内容是一样的,都会包含所有的头文件。正是由于这种机制,出现了一些有趣的问题。首先,由于所有头文件都保留为最终产品,结合HeaderSearchPath中Pods/Headers/Private路径的存在,我们完全可以在其他组件中引用私有头文件。比如我只需要使用#import,它就会命中私有文件的匹配路径。其次,在静态库的情况下,一旦我们启用UseHeaderMap,结合组件中所有头文件的类型都是Project,这个hmap将只包含#import"ClassA.h"的key-valuereferences,也就是只有#import"ClassA.h"的方式才会命中hmap策略,否则会通过HeaderSearchPath找到它的相关路径,比如下图中的PodB。在构建过程中,Xcode会为PodB生成5个hmap文件,也就是说,这5个文件只会在编译PodB时用到,其中PodB会依赖PodA的一些头文件,但是由于PodA中的头文件都是项目类型,hmap中的Key都是ClassA.h,也就是说我们只能通过#import"ClassA.h"的方式导入。而我们也知道,在引用其他组件的时候,一般都是通过#import导入。至于为什么要用这种方式,一方面,这种写法会理清头文件的来历,避免出现问题。另一方面,这个方法可以让我们随意切换是否启用clang模块。当然还有一点,苹果在WWDC中不止一次推荐开发者使用这种方式引入头文件。继续上面的话题,那么对于静态库和以#import标准方式导入头文件的情况,开启UseHeaderMap选项并不能帮助我们提高编译速度。但是真的没有办法使用HeaderMap吗?cocoapods-hmap-prebuilt诞生了当然,总会有解决办法的。我们可以自己制作一个基于CocoaPods规则的hmap文件。正是基于这样的想法,诞生了美团点评开发的cocoapods-hmap-prebuilt插件。!它的核心功能不多,大致有以下几点:组件名/头文件名听上去有点绕,内容有点多,但是你不用关心这些,你只需要需要经过以下两步转换才能使用:在Gemfile中声明插件。在Podfile中使用插件。//thisispartofGemfilesource'http://sakgems.sankuai.com/'dogem'cocoapods-hmap-prebuilt'gem'XXX'...end//thisispartofPodfiletarget'XXX'doplugin'cocoapods-hmap-prebuilt'pod'XXX'...end另外,为了扩展它的实用性,我们还提供了头文件打补丁(解决同名头文件的定向选择)和环境变量注入(在其他系统中使用,无侵入),方便other在不同的场景下使用。至此,cocoapods-hmap-prebuilt的介绍就告一段落了。回首整个故事的开头,HeaderMap是我在研究Swift和Objective-C混合过程中发现的一个非常小的知识点,而Xcode本身已经基于HeaderMap实现了一套功能,在实际使用中过程中,其性能并不理想。但幸运的是,在后续探索的过程中,我们发现了为什么Xcode的HeaderMap没有生效,以及为什么不兼容CocoaPods。虽然它的原理并不复杂,核心点是发现文件读取、读取等IO操作被编译成内存读取操作,但是结合实际的业务场景,我们发现它的好处是非常可观的。或许这是在提醒我们永远对技术保持好奇吧!其实使用ClangModule技术也可以解决本文开头提到的几个问题,但不在本文讨论范围之内。如果你对ClangModule或Swift和Objective-C混合感兴趣,欢迎阅读参考文档中的《从预编译的角度理解 Swift 与 Objective-C 及混编机制》了解更多详情。