写这篇文章的契机是在开发过程中,我发现车e估日常测试打包只需要不到十分钟,而车商则经常大于16分钟,所以就想通过一些方法来减少打包时间。
在讨论打包优化方法之前,我们先了解一下编译过程。iOS编译的过程可以简化为:预处理-编译生成中间代码-汇编器生成汇编代码-生成机器码-链接-生成可执行文件。在我们打包的时候,可以简单的认为打包时间由:编译时间+链接时间+生成调试信息时间。由于链接是没有缓存的,而且只能用单核进行,所以它的耗时主要取决于单核性能和磁盘读写速度,所以这一块除了更换设备,没有什么优化空间。对于调试信息,我们日常测试包可以选择,不生成调试信息(dSYM文件),这样可以减少20秒左右的时间。可以看到,我们可以减少的时间主要放在编译阶段。
1 减少编译文件和资源
由于在编译过程中,所有依赖的文件、系统库、第三方库都会被引入编译,因此一个比较直接的方式就是减少文件和资源的数量,保持代码整洁,对于不需要使用的库和文件,及时进行清理。比如我们这次车e估开发过程中,有一组准备废弃的相机基类,在确定不使用且通过测试之后就删除了此次开发过程中最开始继承该基类的文件,
下图是车商的部分依赖系统库,可以看到还有Twitter这种,所以文件在日积月累的情况下,是会有不少冗余依赖的。
2 减少重复编译
现有项目中,我们的引入依赖文件都是使用#import。由于#import 的本质就是把被依赖头文件的内容拷贝到自己的头文件中来,因此头文件 A 中实际上包含了 M N 个头文件的内容,也就需要 M N 次文件 IO 和相关处理。当项目中每增加一个依赖头文件 A 的文件,就会重复一次上述的 M * N 复杂度的过程。
以前我们会使用PCH文件。PCH 文件的好处是,这个文件中的头文件只会被编译一次并缓存下来,然后添加到项目中所有的头文件中去。上述问题倒是解决了,但很智障的一点是,所有文件都会隐式的依赖所有 PCH 中的文件,而真正需要被全局依赖的文件其实非常少。因此实际开发中,更多的人会把 PCH 当成一种快速 import 的手段,而非编译性能的优化。前文解释过,PCH 文件一旦发生修改,会导致彻彻底底,完完整整的项目重编译,从而降低编译速度。正是因为 PCH 的副作用甚至抵消了它带来的优化,苹果已经默认不使用 PCH 文件了。
用来取代 PCH 的就是 Clang modules 技术,对于开启了这一选项的项目,我们可以用 @import 来替代过去的 #import,比如:
@import UIKit;等价于 #import <UIKit/UIKit.h>。抛开自动链接 framework 这些小特性不谈,Clang modules 可以理解为模块化的 PCH,它具备了 PCH 可以缓存头文件的优点,同时提供了更细粒度的引用。
还有一个小技巧是使用向前声明。在.h文件中使用@class CLASSNAME,而不是#import CLASSNAME.h。将引入头文件的时机尽量延后,只在需要的时候才引入,这样可以减少类使用者所需要引入头文件的数量。而且前向声明也解决了两个类的循环引用问题。
对常用的工具类进行打包(Framework/.a)打包成Framework或者静态库,这样编译的时候这部分代码就不需要重新编译了。在车商跟车e估打包的对比过程中我发现,车商的Realm这个第三方库,每一次编译会花费很长时间,而车e估则没有这个花费,原来是车e估没有使用cocoapods来管理Realm,而是手工引入realm的framework,所以花费时间大大减少。对于稳定的第三方库,或者我们编写的工具,我们也可以采用这种方式来降低打包花费。
由此其实引发了我的一点思考,对于iOS开发来说,我们一般用cocoapods来管理第三方库,但是这个管理意义是什么呢?我觉得最重要是为了管理各种依赖配置,不用再去手动配置,其次是为了保持第三方库的优化、更新和bug修复。在我们的项目中cocoapods其实只是用了他的第一项功能,那么我的想法就是可以对一部分编译很耗时的第三方库手工引入和管理依赖,这个其实不用花太多功夫,但是可以为以后的每一次打包节省不少时间。
3 缓存编译
其实如果我们每次打包之前不clean的话,我们会发现打包时间其实会缩短很多的。这是因为Xcode本身使用了增量编译的方式,每次编译会重新编译修改文件和修改文件的引用,但是对于没有改动的部分则会使用之前缓存的编译结果。但是这个增量编译并不稳定,很久之前在打包过程中出现过增量编译没有打包最新代码的问题。现在的增量编译应该是越来越稳定的,但是个人对于这个稳定问题的担心仍然存在。
因此出现了一些第三方缓存工具,比较容易接入的是ccache这个工具。ccache是一个能够把编译的中间产物缓存起来的工具,而且比较稳定,因此可以大大提高编译速度。但是ccache不支持clang module,因此需要关闭项目中的Enable Modules,同时对于cocoapods管理的第三方库,也需要处理clang module的问题。具体使用方法可以参考文章使用ccache让打包飞起来。 ps:我没有配置成功,在配置过程中找不到对应的编译器。但是根据其他程序员的实践,这种缓存编译的方式可以提升五倍以上的打包时间,以后有时间再继续研究一下。
4 修改工程设置
一般来说,我们的持续集成工具主要是用来给产品经理或者测试人员使用,用来体验功能或者验证 Bug,除非是需要上架 App Store,否则并不需要关心运行时性能。然而在手机上使用的 Release 模式,默认会开启各种优化,这些优化都是牺牲编译性能,换取运行时速度,对于上架的包而言无可厚非,但对于那些 Daily Build 包来说,就显得得不偿失了。
因此,加速打包的思路和优化的思路是完全互逆的,我们要做的就是关闭一切可能的优化。
Valid Architectures
这个选项是指定处理器的指令集。对于功能测试来说,我们可以指定只打包测试机对应的指令集。
armv7|armv7s|arm64|arm64e都是ARM处理器的指令集
i386|x86_64 是Mac处理器的指令集
指令集对应的机型:
2018 A12芯片arm64e : iphone XS、 iphone XS Max、 iphoneXR
2017 A11芯片arm64: iPhone 8, iPhone 8 Plus, and iPhone X
2016 A10芯片arm64:iPhone 7 , 7 Plus, iPad (2018)
2015 A9芯片arm64: iPhone 6S , 6S Plus
2014 A8芯片arm64: iPhone 6 , iPhone 6 Plus
2013 A7芯片arm64: iPhone 5S
armv7s:iPhone5|iPhone5C|iPad4(iPad with Retina Display)
armv7:iPhone4|iPhone4S|iPad|iPad2|iPad3(The New iPad)|iPad mini|iPod Touch 3G|iPod Touch4
模拟器32位处理器测试需要i386架构,
模拟器64位处理器测试需要x86_64架构,
真机32位处理器需要armv7,或者armv7s架构,
真机64位处理器需要arm64架构。
Optimization Level
关闭编译优化。优化的基本原理是牺牲编译时性能,追求运行时性能。常见的优化有编译时删除无用代码,保留调试信息,函数内联等等。因此提升打包速度的秘诀就是反其道而行之,牺牲运行时性能来换取编译时性能。对于日常测试打包,可以将Optimize level 的Release状态下的值改成 O0,表示不做任何优化。
Debug Information Format
不生成 dYSM 文件,Release状态下的值修改为DWARF。
5 采用新构建系统(New Build System)
苹果从Xcode 9开始推出了新构建系统(New Build System),并在Xcode 10使用其为默认构建系统来替代旧构建系统(Legacy Build System)。采用新构建系统能够减少构建时间。
简要介绍一下原理,对于旧构建系统,当我们构建一个程序的时候,会明确所需要构建的所有Target,这些Target之间的依赖关系,以及这些Target构建的顺序。采用顺序会造成多处理器系统资源的浪费,从而表现为编译时间的浪费,解决这个问题的方式就是采用并行编译,这也是新构建系统优化的核心思想。详细了解新构建系统,探究Xcode New Build System对于构建速度的提升。
6 使用脚本
总结
上面就是总结的一些提高打包速度的一些方法,应该根据项目实际情况选择合适的方法优化,对于中小型尽量将打包时间控制在十分钟以内。对于项目改动较小且比较容易实现的方式主要是:减少编译文件和资源、 减少重复编译以及工程配置修改这几种方式。