前言
公司虽然有专门的打包机,但是打包机打一次包总耗时30min左右。 在平常可能还没有什么关系,但是到了回归阶段就很难受了。
测试回归阶段发现一个bug,等开发定位问题->修复->打包->测试验证,至少是一个小时的周期。
更可怕的是才打了一个包,测试又发现一个bug……
目前编译时间已经影响到了开发效率。2021年上半年,和同事一起做了编译优化
注:这篇文章并没有去解决增量编译问题,因为增量编译目前Xcode做的已经足够好,在开发过程中瓶颈并不大;而打包机每次都是全量编译,我们这里主要是解决全量编译耗时。
了解编译原理和过程
编译原理
苹果使用的是LLVM
来编译项目的,其中Clang
作为前端处理。
本篇偏重于实践操作,具体原理可以看这里: 传送门
从Xcode
编译日志查看项目的编译顺序。
从这里可以看到相对来说整体全面的编译信息。也可以看的某个文件的编译耗时。
这里我有一个未解的问题:每个文件后面的耗时是什么意思?为什么各个文件的耗时总和与总时间对不上?求大神解答
个人猜测是这里是利用多核CPU并发执行的结果。
我们根据Xcode
的编译日志,总结了我们项目的编译大致过程:
对于某个具体文件的编译过程可以通过Clang
来查看:
使用clang
-ccc-print-phases
-framework Foundation
AFNetworking-dummy.m
-o
AFNetworking-dummy.m
命令,终端执行得到下面信息:
使用Clang
的Time_Trace
功能来查看
苹果编译使用的是LLVM
。因为开源的LLVM
已经有大神加入了各个节点的耗时埋点。所以我们做的第一步是使用开源的LLVM
来编译一次项目,拿到分析报告。
具体操作如下:
1.下载开源的LLVM
注:当前的Xcode自带Clang应该已经支持,所以其实不需要下载,可以利用终端 clang --version
查看版本以及路径。 但是自己在实践过程中并未成功,因此还是使用了下载的。
2.设置Xcode
的Build settings
里 User-Defined
3.设置生成编译耗时的参数
4.为了防止报错,修改这个参数
5.进行项目编译
6.编译后查找对应的json分析文件
文件路径在图下面的红色框里
7.使用chrome://tracing
或者 https://www.speedscope.app
查看对应的Json文件进行分析
拿到项目里的耗时图
下图的分析数据是经过自己写的python脚本聚合,其他人可以参考这篇博客的脚本
前端
耗时是3876s 占比:96%
source
的耗时是3562s 占整体比为:88%
module load
是 1710s
说明我们的耗时还是集中在了source
环节上,因此解决掉source
这一环节,就能很大提升项目的编译速度。通过这篇微信的优化文章了解到,这个环节主要是耗时在头文件处理上。
关于module Load
这个是Xcode
增加的一个特性。控制开关在Xcode的Build settings
里面:
苹果文档介绍:Enables the use of modules for system APIs. System headers are imported as semantic modules instead of raw headers. This can result in faster builds and project indexing.
专门对一个工程进行了开启和关闭验证。
打开这个开关有两个好处:
1.系统的类库不需要再显示的在Xcode的 build settings 中引入对应类库
2.通过编译耗时对比,明显对于类的编译减少了很多(对于一个自定义的View,开启的情况下编译耗时150ms左右,关闭的情况下解决400ms)。
复制代码
编译时间消耗点
-
每个引入的文件都会进行编译;
-
每个引入的资源文件都需要copy进入包里;
-
每个xib或者storyboard文件也需要编译并copy进包里
-
全量编译的情况下,每个pod库每次都要重新编译
-
在编译中有多个脚本执行
-
编译过程中,需要拷贝swift的标准库(这个耗时从
Xcode
中看,一个指令集下都要耗时30s多)
……
减少编译时间的思路:能不编译就不编译
1.减少无用文件和无用资源
-
删除无用的库
-
删除无用的文件
可以从可执行文件里,找到所有类的集合
和引用的类的集合
,做一个差值拿到未使用的类的集合
,可以参考:iOS 脚本查看项目中未使用的类
这种方式不适用于我们的项目。因为我们项目使用的是路由跳转,跳转指令都由服务端下发。
-
删除无用图片
这个网上有很多脚本,比如:
LSUnusedResources
2.优化头文件引用
-
增加
pch
文件pch
文件本来被苹果抛弃,原因是容易引起滥用,滥用pch
的结果是导致编译耗时增加。但是如果使用得当,能够极大提升开发效率并且减少编译时长。
-
写一个算法,分析出头文件被引用的次数
-
将头文件被引入最多的头文件放入pch中
这里有个疑问:
pch
能在预处理阶段进行完,将pch
里的头文件内容全部替换到所有头文件中。按照这样的情况,难道不是应该拖慢编译速度吗?但是事实上,增加
pch
文件后,我们的编译时长缩短了2min
左右。 -
-
项目分模块给对应的开发,负责将头文件引入优化
这一步优化实际落地很耗时,但是因为对增量编译也有好处,需要长期进行。
-
去掉不必要的文件引入
-
头文件中尽量使用
@class
来标识类
为什么这一步可以节省时间?头文件放在.h和.m中有什么区别?
其实从编译角度考虑,区别并不大。因为都会在预处理阶段,将头文件内容导入进去。 但是放在.h中后,增大了该文件被其他不需要引入的类引用的风险。项目中引用很乱,因此处理这一步也能优化不少。
-
3.使用缓存,减少不必要的编译。
使用CC-cache
原理: 将Xcode编译文件缓存下来,在编译的时候减少不必要的重复编译。
这一步对于全量编译来说,优化最大。
我们项目里使用后,直接减少了10min
的编译时间。
但是需要注意的是,它不支持上面提到的Module Enable
相关资料可以参考:传送门
4.hmap插件
原理: 编译时候要找到对应的文件,但是这个查找过程十分耗时。虽然Xcode
自带了hmap
的功能,但是相关条件下,并不能提升编译速度。使用hmap
相关插件解决问题后,就可以省去pod
库相关文件查找时间,减少编译时长。
相关原理请看这里:一款可以让大型iOS工程编译速度提升50%的工具
我们项目使用的插件是:传送门
这一步使用后,我们在原先的基础上又优化了2~3min
总结
站在巨人的肩膀上,我们通过努力将全量编译时间从22min
左右,减少到了9min
左右。后面还有很多其他优化空间,需要继续努力(比如:优化头文件引用,模块化,Xcode相关编译配置等等)。