关于ios:一款可以让大型iOS工程编译速度提升50的工具

3次阅读

共计 7848 个字符,预计需要花费 20 分钟才能阅读完成。

cocoapods-hmap-prebuilt 是什么?

cocoapods-hmap-prebuilt 是美团平台迭代组自研的一款 cocoapods 插件,以 Header Map 技术 为根底,进一步晋升代码的编译速度,欠缺头文件的搜寻机制。

尽管以二进制组件的形式构建 App 是 HPX(美团挪动端对立继续集成 / 交付平台)的支流解决方案,但在某些场景下(Profile、Address/Thread/UB/Coverage Sanitizer、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

也正是通过这样的目录构造和软链,CocoaPods 得以在 Header Search Path 中增加如下的参数,使得预编译环节顺利进行。

$(inherited)
${PODS_ROOT}/Headers/Private
${PODS_ROOT}/Headers/Private/AFNetworking
${PODS_ROOT}/Headers/Public
${PODS_ROOT}/Headers/Public/AFNetworking

尽管这种构建 Search Path 的形式解决了预编译的问题,但在某些我的项目中,例如多达 400+ 组件的巨型我的项目中,会造成以下几点问题:

  1. 大量的 Header Search Path 门路,会造成编译参数中的 -I 选项极速收缩,在达到肯定长度后,甚至会造成无奈编译的状况
  2. 目前美团的工程中,曾经有近 5W 个头文件,这意味着不论是头文件的搜寻过程,还是软链的创立过程,都会引起大量的文件 IO 操作,进而会产生一些耗时操作。
  3. 编译工夫会随着组件数量急剧增长,以美团和公众点评有 400+ 个组件的体量为参考,全源码打包耗时均在 1 小时以上。
  4. 基于门路程序查找头文件的形式有潜在的危险,例如重名头文件的状况,排在前面的头文件永远无奈参加编译。
  5. 因为 ${PODS_ROOT}/Headers/Private 门路的存在,让援用其余组件的公有头文件变为了可能。

想解决上述的问题,好一点的状况下,可能会节约 1 个小时,而不好的状况,就是让有危险的代码上线了,你说工程师会不会因而而感到头疼?

Header Map 是个啥?

还好 cocoapods-hmap-prebuilt 的呈现,让这些问题变成了历史,不过要想了解它为什么能解决这些问题,咱们得先了解一下什么是 Header Map。

Header Map 其实是一组头文件信息映射表!

为了更直观的了解 Header Map,咱们能够在 Build Setting 中开启 Use Header Map 选项,实在的体验一下它。

而后在 Build Log 里获取相应组件里对应文件的编译命令,并在最初加上 -v 参数,来查看其运行的机密:

$ clang <list of arguments> -c some-file.m -o some-file.o -v

在 console 的输入内容中,咱们会发现一段有意思的内容:

通过下面的图,咱们能够看到编译器将寻找头文件的程序和对应门路展现进去了,而在这些门路中,咱们看到了一些生疏的货色,即后缀名为 .hmap 的文件,前面还有个括号写着 headermap。

没错!它就是 Header Map 的实体。

此时 Clang 曾经在方才提到的 hmap 文件里塞入了一份头文件名和头文件门路的映射表,不过它是一种二进制格局的文件,为了验证这个的说法,咱们能够通过 milend 编写的 hmap 工具来查其内容。

在执行相干命令(即 hmap print)后,咱们能够发现这些 hmap 里保留的信息结构大抵如下, 相似于一个 Key-Value 的模式,Key 值是头文件的名称,Value 是头文件的理论物理门路:

须要留神,映射表的键值内容会随着应用场景产生不同的变动,例如头文件援用是在 "..." 的模式下,还是 <...> 的模式下,又或是在 Build Phase 里 Header 的配置状况。例如,你将头文件设置为 Public 的时候,在某些 hmap 中,它的 Key 值就为 PodA/ClassA,而将其设置为 project 的时候,它的 Key 值可能就是 ClassA,而配置这些信息的中央,如下图所示:

至此我想你应该理解到 Header Map 到底是个什么货色了。

当然这种技术也不是一个什么新鲜事儿,在 Facebook 的 buck 工具中也提供了相似的货色,只不过文件类型变成了 HeaderMap.java 的样子。

此时,我预计你可能并不会对 buck 产生太多的趣味,而是开始思考上一张图中 Headers 的 Public、Private、Project 到底代表着什么意思,如同很多同学素来没怎么关注过,以及为什么它会影响 hmap 里的内容?

Public,Private,Project 是个啥?

在 Apple 官网的 Xcode Help – What are build phases? 文档中,咱们能够看到如下的一段解释:

Associates public, private, or project header files with the target. Public and private headers define API intended for use by other clients, and are copied into a product for installation. For example, public and private headers in a framework target are copied into Headers and PrivateHeaders subfolders within a product. Project headers define API used and built by a target, but not copied into a product. This phase can be used once per target.

总的来说,咱们能够晓得一点,就是 Build Phases – Headers 中提到 Public 和 Private 是指能够供外界应用的头文件,而 Project 中的头文件是不对外应用的,也不会放在最终的产物中。

如果你持续翻阅一些材料,例如 StackOverflow – Xcode: Copy Headers: Public vs. Private vs. Project? 和 StackOverflow – Understanding Xcode’s Copy Headers phase,你会发现在晚期 Xcode Help 的 Project Editor 章节里,有一段名为 Setting the Role of a Header File 的段落,外面具体记录了三个类型的区别。

Public: The interface is finalized and meant to be used by your product’s clients. A public header is included in the product as readable source code without restriction.
Private: The interface isn’t intended for your clients or it’s in early stages of development. A private header is included in the product, but it’s marked“private”. Thus the symbols are visible to all clients, but clients should understand that they’re not supposed to use them.
Project: The interface is for use only by implementation files in the current project. A project header is not included in the target, except in object code. The symbols are not visible to clients at all, only to you.

至此,咱们应该可能彻底理解了 Public、Private、Project 的区别。简而言之,Public 还是通常意义上的 Public,Private 则代表 In Progress 的含意,至于 Project 才是通常意义上的 Private 含意。

此时,你会不会联想到 CocoaPods 中 Podspec 的 Syntax 里还有 public_header_filesprivate_header_files 两个字段,它们的实在含意是否和 Xcode 里的概念抵触呢?

这里咱们仔细阅读一下官网文档的解释,尤其是 private_header_files 字段。

咱们能够看到,private_header_files 在这里的含意是说,它自身是绝对于 Public 而言的,这些头文件转义是不心愿裸露给用户应用的,而且也不会产生相干文档,然而在构建的时候,会呈现在最终产物中,只有既没有被 Public 和 Private 标注的头文件,才会被认为是真正的公有头文件,且不呈现在最终的产物里。

看起来,CocoaPods 对于 Public 和 Private 的官网解释是和 Xcode 中的形容统一的,两处的 Private 并非咱们通常了解的 Private,它的本意更应该是开发者筹备对外开放,但又没齐全 Ready 的头文件,更像一个 In Progress 的含意。

这一块是不是让你有点大跌眼镜?那么,在事实世界中,咱们是否正确的应用了它们呢?

为什么用原生的 hmap 不能改善编译速度?

后面咱们介绍了 hmap 是什么,以及怎么开启它(启用 Build Setting 中的 Use Header Map 选项),也介绍了一些影响生成 hmap 的因素(Public、Private、Project)。

那是不是我只有开启 Xcode 提供的 Use Header Map 就能够晋升编译速度了呢?

很惋惜,答案是否定的!

至于起因,咱们就从上面的例子开始说起,假如咱们有一个基于 CocoaPods 构建的全源码工程项目,它的整体构造如下:

  • 首先,Host 和 Pod 是咱们的两个 Project,Pods 下的 Target 的产物类型为 Static Library。
  • 其次,Host 底下会有一个同名的 Target,而 Pods 目录下会有 n+1 个 Target,其中 n 取决于你依赖的组件数量,而 1 是一个名为 Pods-XXX 的 Target,最初,Pods-XXX 这个 Target 的产物会被 Host 里的 Target 所依赖。

整个构造看起来如下所示:

当构建的产物类型为 Static Library 的时候,CocoaPods 在创立头文件产物过程中,它的逻辑大抵如下:

  • 不管 podspec 里如何设置 public_header_filesprivate_header_files,相应的头文件都会被设置为 Project 类型。
  • Pods/Headers/Public 中会保留所有被申明为 public_header_files 的头文件。
  • Pods/Headers/Private 中会保留所有头文件,不论是 public_header_files 或者 private_header_files 形容到,还是那些未被形容的,这个目录下是以后组件的所有头文件选集。
  • 如果 podspec 里未标注 Public 和 Private 的时候,Pods/Headers/PublicPods/Headers/Private 的内容一样且会蕴含所有头文件。

正是因为这种机制,会导致一些有意思的问题产生。

  • 首先,因为所有头文件都被当做最终产物保留下来,在联合 Header Search Path 里 Pods/Headers/Private 门路的存在,咱们齐全能够援用到其余组件里的公有头文件,例如我只有应用 #import <SomePod/Private_Header.h> 的形式,就会命中公有文件的匹配门路。
  • 其次,就是在 Static Library 的情况下,一旦咱们开启了 Use Header Map,联合组件里所有头文件的类型为 Project 的状况,这个 hmap 里只会蕴含 #import "ClassA.h" 的键值援用,也就是说只有 #import "ClassA.h" 的形式才会命中 hmap 的策略,否则都将通过 Header Search Path 寻找其相干门路,例如下图中的 PodB,在其 build 的过程中,Xcode 会为 PodB 生成 5 个 hmap 文件,也就是说这 5 个文件只会在编译 PodB 中应用,其中 PodB 会依赖 PodA 的一些头文件,但因为 PodA 中的头文件都是 Project 类型的,所以其在 hmap 里的 Key 全副为 ClassA.h,也就是说咱们只能以 #import "ClassA.h" 的形式引入。

而咱们也晓得,在援用其余组件的时候,通常都会采纳 #import <A/A.h> 的形式引入。至于为什么会用这种形式,一方面是这种写法会明确头文件的由来,防止问题,另一方面也是这种形式能够让咱们在是否开启 clang module 中随便切换。当然,还有一点就是 Apple 在 WWDC 里已经不止一次倡议开发者应用这种形式来引入头文件。

接着下面的话题来说,所以说在 Static Library 的状况下且以 #import <A/A.h> 这种规范形式引入头文件时,开启 Use Header Map 选项并不会帮咱们晋升编译速度。

但真的就没有方法应用 Header Map 了么?

cocoapods-hmap-prebuilt 诞生了

当然,总是有方法解决的,咱们齐全能够本人动手做一个基于 CocoaPods 规定下的 hmap 文件,正是基于这个想法,美团自研的 cocoapods-hmap-prebuilt 插件诞生了!

它的外围性能并不多,大略有以下几点:

  • 借助 CocodPods 解决 Header Search Path 和创立头文件 soft link 的机会,构建了头文件索引表并以此生成 n+1 个 hmap 文件(n 是每个组件本人的 Private Header 信息,1 是所有组件公共的 Public Header 信息)。
  • 重写 xcconfig 文件里的 Header Search Path 到对应的 hmap 文件上,一条指向组件本人的 private hmap,一条指向所有组件共用的 public hmap。
  • 针对 public hmap 里的重名头文件进行了非凡解决,只容许保留 组件名 / 头文件名 形式的 Key-Value,排查重名头文件带来的异样行为。
  • 将组件本身的 Ues Header Map 性能敞开,缩小不必要的文件创建和读取。

听起来可能有点绕,内容也有点多,不过这些你都不必关怀,你只须要通过以下 2 个步骤就能将其应用起来:

  1. 在 Gemfile 里申明插件。
  2. 在 Podfile 里应用插件。
// this is part of Gemfile
source 'http://sakgems.sankuai.com/' do
  gem 'cocoapods-hmap-prebuilt'
  gem 'XXX'
  ...
end

// this is part of Podfile
target 'XXX' do
  plugin 'cocoapods-hmap-prebuilt'
  pod 'XXX'
  ...
end

除此之外,为了拓展其实用性,咱们还提供了头文件补丁(解决重名头文件的定向选取)和环境变量注入(无侵入的在其余零碎中应用)的能力,便于其在不同场景下的应用。

总结

至此,对于 cocoapods-hmap-prebuilt 的介绍就要完结了。

回看整个故事的开始,Header Map 是我在钻研 Swift 和 Objective-C 混编过程中发现的一个很小的知识点,而且 Xcode 本身就实现了一套基于 Header Map 的性能,在理论的应用过程中,它的体现并不现实。

但侥幸的是,在后续的摸索的过程中,咱们发现了为什么 Xcode 的 Header Map 没有失效,以及为什么它与 CocoaPods 呈现了不兼容的状况,尽管它的原理并不简单,外围点就是将文件查找和读取等 IO 操作编变成了内存读取操作,但结合实际的业务场景,咱们发现它的收益是非常可观的。

或者这是在揭示咱们,要永远对技术放弃一颗好奇的心!

其实,利用 Clang Module 技术也能够解决本文一开始提到的几个问题,但它并不在这篇文章的探讨范畴中,如果你对 Clang Module 或者对 Swift 与 Objective-C 混编感兴趣,欢送浏览参考文档中的《从预编译的角度了解 Swift 与 Objective-C 及混编机制》一文,以理解更多的详细信息。

参考文档

  • Apple – WWDC 2018 Behind the Scenes of the Xcode Build Process
  • Apple 的 HeaderMap.cpp 源码
  • 美团技术学院 - 从预编译的角度了解 Swift 与 Objective-C 及混编机制

作者

  • 思琦,笔名 SketchK,美团 iOS 工程师,目前负责挪动端 CI/CD 方面的工作及平台内 Swift 技术相干的事宜。
  • 旭陶,美团 iOS 工程师,目前负责 iOS 端开发提效相干事宜。
  • 霜叶,2015 年退出美团,先后从事过 Hybrid 容器、iOS 根底组件、iOS 开发工具链和客户端继续集成门户零碎等工作。

| 想浏览更多技术文章,请关注美团技术团队(meituantech)官网微信公众号。

| 在公众号菜单栏回复【2020 年货】、【2019 年货】、【2018 年货】、【2017 年货】、【算法】等关键词,可查看美团技术团队历年技术文章合集。

正文完
 0