引言
本文旨在记录一次使用 CCache 对 Xcode Build 时间做优化的过程,并简单的描述一下用法,总结一下其他使用到的优化方案,详细记录过程中涉及到的一些对于个人来说重要的基础知识点,以及实践中遇到的问题,以及最后解决问题的办法
背景
相信上面这张图最能代表我做这次优化的背景了,Clean 之后的一次 Build 时间达到了10min,在没办法通过硬件解决的情况下,只好寻找通过软件解决的途径了;并且这只是我们平时开发过程遇到的背景,其实还有就是打包的时间也是巨长;因此,再这样的情况下,我们不得不进行一些些优化
基础知识
这里的基础知识主要是为了为后面优化过程中的涉及到的问题做铺垫
Build 过程
预处理
'#import 的展开,这一步做的事情便是告诉处理器将我们引入的各个.h文件插入到 #import 的位置
宏定义的替换,这个好理解,不用解释
当然,预处理完了之后会进行词法分析啥的,这里就不一一提到,下面也是,就说说主要的一些过程
- 编译
编译过程将用户可识别的语言翻译成一组处理器可识别的操作码,通常翻译成汇编语言
- 汇编
汇编器将可读的汇编代码转换为机器代码。它会创建一个目标对象文件,一般简称为 对象文件。也就是后缀是 .obj 或者 .o 目标文件
- 链接(各种需要的FrameWork,Foundation.framework等之类)
将目标文件和库文件关联起来生成可执行文件
@import && #import && PCH文件
import ,先说 #include,#include 做的事情其实就是简单的复制粘贴,将目标.h文件中的内容一字不落地拷贝到当前文件中,并替换掉这句 #include,而 #import 做的事情和 #include 是一样的,只不过 OC 为了避免重复引用可能带来的编译错误,比如B和C都引用了A,D又同时引用了B和C,这样A中定义的东西就在D中被定义了两次,重复了,而加入了 #import,从而保证每个头文件只会被引用一次。
所以,#import 还是拷贝粘贴,这样就带来一个问题:当引用关系很复杂,或者一个头文件被非常多的实现文件引用时,编译时引用所占的代码量就会大幅上升(因为被引用的头文件在各个地方都被copy了一遍)
于是就出来 PCH (预编译头文件),将公用的头文件放入预编译头文件中预先进行编译,然后在真正编译工程时再将预先编译好的产物加入到所有待编译的 Source 中去,来加快编译速度。iOS 开发中 Supporting Files 组内的 .pch 文件就是一个预编译头文件
@import Apple在 LLVM5.0 引入了一个新的编译符号 @import,使用@符号将告诉编译器去使用 Modules 的引用形式,从而获取好处,比如想引用 Mapbox,可以写成
@import Mapbox;
在使用上,这将等价于以前的#import<Mapbox/Mapbox.h>
,但是将使用Modules的特性。
什么是 Moudles? Modules 相当于将框架进行了封装,然后加入在实际编译之时加入了一个用来存放已编译添加过的 Modules 列表。如果在编译的文件中引用到某个 Modules 的话,将首先在这个列表内查找,找到的话说明已经被加载过则直接使用已有的,如果没有找到,则把引用的头文件编译后加入到这个表中。这样被引用到的 Modules 只会被编译一次
动态库与静态库
这两个东西都是编译好的二进制文件。都是不用再编译了的,用法不同而已。更多关于区别,可以看这里
分析问题
好了,铺垫了一波简单的基础知识,可以开始分析问题了:
Build 时间太长,先来看一下 Build 的 log ,看一下到底哪些东西耽误了时间
足足等了10多分钟,一直看着屏幕,发现并不是哪个库或者哪个具体的操作耗时很长,结论就是文件6000多个,文件多导致的编译时间长!
解决问题
好了,初步判断就是文件太多了,开始动手:
还记得之前在网上看到各种大佬通过设置一些 Build Settings,赶紧翻出来看看:
- 1.Optimization Level
这个是 Xcode Build Setting 里的一个参数,Optimization Level 是指编译器的优化层度 它一共有以下几个选项:
None: 编译器不进行任何代码优化
Fast: 编译器进行小幅度代码优化,同时消耗更多的内存
Faster: 会进行所有可用的优化选项而不用花费额外的时间和内存。该选项不会执行循环展开或者内嵌函数。该选项会在提升代码性能的同时增加编译时间
Fastest: 该选项会作出尽可能多的尝试来提高编译性能。但同时会和内嵌函数机制存在冲突。一般不建议使用该选项。
Fastest, Smallest: 编译器会进行所有可用的优化而不显著的增加运行空间。该选项是打包代码的最优选项
Fastest, Aggressive Optimizations:寻求大家帮助????各种找都没找到
所以说我们平时开发的时候可以选择使用 None 来不给代码执行优化,这样既可以减少编译时间,而你的release 版应该选择 Fastest, Smalllest,这样既能执行所有的优化而不增加代码长度,又能使执行文件占用更少的内存。
-
2.Debug Information Format
这一项设置的是是否将调试信息加入到可执行文件中,改为 DWARF 后,如果程序崩溃,将无法输出崩溃位置对应的函数堆栈,但由于 Debug 模式下可以在 Xcode 中查看调试信息,所以改为 DWARF 影响并不大。这一项更改完之后,可以大幅提升编译速度。
其实 Debug Information Format 就是表示是否生成.dSYM文件,也就是符号表。如果为 DWARF 就表示不生成.dSYM文件。
如果在使用 instrument 的时候记得把这个打开,不然你看到的就不是一个个的你看得懂的函数调用栈,就像没有进行符号化的崩溃日志
- 3.将Build Active Architecture Only 改为Yes
这一项设置的是是否仅编译当前架构的版本,如果为 No,会编译所有架构的版本。需要注意的是,此选项在Release 模式下必须为No,否则发布的 ipa 在部分设备上将不能运行
=========悲伤的事情就是,上述的设置,在优化之前早就设置好了,所以,继续接着优化=========
反过来思考一下:咱们主要的问题是编译的文件太多,于是目的就是减少文件的编译不就好了:
- 4.对部分一些常用的工具打包成静态库,这样这部分代码就不用再编译了
可能是个人能力原因,大致看了一下,零零散散的能打包的都打包了,仿佛这一步也不大行得通。
-
5.既然原因是文件太多,那就简单点,直接清理不要的类呗
工具比较多,方法也多,可以借鉴微信的瘦身实践的里面提到的清理无用类,使用otool和link map我大致实践了一下,实在复杂。索性,立马使用 AppCode 的 Inspect 功能对工程分析一波,结果感人:
无用类没有!没有!但是无用的#import倒是一大堆,那么问题就来了,稍微动一下头文件的引入,有可能导致大面积的重新编译,可以优化!!! -
6.清理无用图片资源
通过查看 build 的log,会发现其实还有一部分时间在拷贝图片资源以及一些别的文件,于是,清理无用的资源,是不是又可以瘦身,又可以减少build时间,这里使用的是LSUnusedResources
这里得到的结果就是,在默认的筛选条件下,找到了10M
上述减少资源优化基本都是体力活,可能需要持续的实践,为了看到快速的效果,于是,准备实践一下之前看到的 CCache
- 7.CCache 编译缓存(终于可以扣住文章主题了🤣🤣🤣)
什么是CCache:
CCache 是一个能够把编译的中间产物缓存起来的工具,它会在实际编译之前先检查缓存。
根据bestswifter的这篇文章其实有提到,在我们平时的开发环境中,Xcode其实自己会做增量编译,也就是说默认会使用上次编译留下的缓存,但是在进行持续集成的时候,缓存不被推荐使用,但这是因为苹果的缓存不稳定,某些情况下依然有bug的原因。因此我们只能手动删除 Derived Data 文件夹,还是调用 xcodebuild clean 命令,都会把缓存清空。或者直接使用 xcodebuild archive,会自动忽略缓存。每次都要全部重编译,因此时间当然慢了哦。
那么,要是我们有一个把编译缓存做的很好的东西,是不是就可以好很多了~~
接入 CCache 的教程参见 贝聊科技CCache,为方便阅读,这里做搬运工作:
安装CCache
通过 Homebrew 安装 CCache, 在命令行中执行
$ brew install ccache
命令执行无异常便是安装成功
创建 CCache 编译脚本
为了能让 CCache 介入到整个编译的过程,我们要把 CCache 作为项目的 C 编译器,当 CCache 找不到编译缓存时,它会再把编译指令传递给真正的编译器 clang。
新建一个文件命名为 ccache-clang
$ touch ccache-clang
然后内容为下面这段脚本,放到你的项目里
#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
export CCACHE_MAXSIZE=10G
export CCACHE_CPP2=true
export CCACHE_HARDLINK=true
export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
# 指定日志文件路径到桌面,等下排查集成问题有用,集成成功后删除,否则很占磁盘空间
export CCACHE_LOGFILE='~/Desktop/CCache.log'
exec ccache /usr/bin/clang "$@"
else
exec clang "$@"
fi
在命令行中,cd 到 ccache-clang 文件的目录,把它的权限改成可执行文件
$ chmod 777 ccache-clang
如果你的代码或者是第三方库的代码用到了C++,则把 ccache-clang这个文件复制一份,重命名成 ccache-clang++。相应的对clang的调用也要改成clang++,否则 CCache 不会应用在 C++ 的代码上。
#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
export CCACHE_MAXSIZE=10G
export CCACHE_CPP2=true
export CCACHE_HARDLINK=true
export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
# 指定日志文件路径到桌面,等下排查集成问题有用,集成成功后删除,否则很占磁盘空间
export CCACHE_LOGFILE='~/Desktop/CCache.log'
exec ccache /usr/bin/clang++ "$@"
else
exec clang++ "$@"
fi
成功之后项目根目录下面应该有这两个文件
Xcode 项目的调整
- 定义CC常量
在你项目的构建设置 (Build Settings)中,添加一个常量 CC,这个值会让 Xcode 在编译时把执行路径的可执行文件当做 C 编译器。
CC 常量的值为
$(SRCROOT)/ccache-clang
,如果你的脚本不是放在项目根目录,则自行调整路径。如果一运行项目就报错,检查下路径是不是填错了。
- 关闭 Clang Modules,这一步真的很恶心
因为 CCache 不支持 Clang Modules,所以需要把 Enable Modules 的选项关掉。这个问题在 CocoaPods 上如何处理,后面会讲。
关闭了 Enable Modules 后需要作出的调整
因为关闭了 Enable Modules,所以必须删除所有的 @import语句,替换为#import的语法例如将 @import UIKit 替换为 #import <UIKit/UIKit.h>。之后,如果你用到了其他的系统框架例如 AVFoundation、CoreLocation等,现在 Xcode 不会再帮你自动引入了,你得要在项目 Target 的 Build Phrase -> Link Binary With Libraries 里面自己手动引入。
- CocoaPods 的 处理
如果你的项目不用 CocoaPods 来做包管理,那你已经完全接入成功了,不用执行下面的操作。
因为 CocoaPods 会单独把第三方库打包成一个 Static Library(或者是Dynamic Framework,如果用了 use_frameworks!选项),所以 CocoaPods 生成的 Static Library 也需要把 Enable Modules 选项给关掉。但是因为 CocoaPods 每次执行 pod update 的时候都会把 Pods 项目重新生成一遍,如果直接在 Xcode 里面修改 Pods 项目里面的 Enable Modules 选项,下次执行pod update的时候又会被改回来。我们需要在 Podfile 里面加入下面的代码,让生成的项目关闭 Enable Modules 选项,同时加入 CC 参数,否则 pod 在编译的时候就无法使用 CCache 加速:
post_install do |installer_representation|
installer_representation.pods_project.targets.each do |target|
target.build_configurations.each do |config|
#关闭 Enable Modules
config.build_settings['CLANG_ENABLE_MODULES'] = 'NO'
# 在生成的 Pods 项目文件中加入 CC 参数,路径的值根据你自己的项目来修改
config.build_settings['CC'] = '$(PODS_ROOT)/../ccache-clang'
end
end
end
需要注意的是,如果你使用的某个 Pod 引用了系统框架,例如AFNetworking引用了System Configuration,你需要在你自己项目的Build Phrase -> Link Binary With Libraries里面代为引入,否则你编译时可能会收到 Undefined symbols xxx for architecture yyy一类的错误。有点回到了原始时代的感觉,但考虑到编译速度的极大提升,这一点代价可以接受。
好了,到目前为止,你可以开始 cmd+b
了,第一次会比较慢,第二次或者往后,你就会发现 cache hit 变大了,随着它的变大,时间你会发现越来越少
好了,看看集成了 CCache 的效果!!!!!,你没看错,时间真的少了一半
你以为文章就到此截止了?不行,还有问题没有解决:
- 不支持 PCH 文件 怎么办?一定要移除么,可我真的不想移除
其实贝聊有提到,当你修改了PCH或者PCH引用的到的头文件时,会造成缓存失效,只能全部重新编译,所以,只要你不会频繁的更改PCH文件的话,或者不改,其实问题都不大,还是可以接受的,起码能享受到CCache带来的快感
小结
本文旨在对 Build 优化过程做一个记录,记录在优化过程中遇到的一个知识点以及困惑,有些个人的理解也穿插在其中,如果个人理解有误,还望探讨指正