如何将iOS项目编译速度提升5倍业务代码约23万行,私有库约6万行,第三方库代码约15万行,单个客户端代码行数为大约600,000。现在打包一次需要11-12分钟。虽然和Facebook的40分钟相去甚远,但我们在内测的时候,经常一天发布两三次内测版。打包的时候,CPU占用率基本是100%。因为没有专门的CI机器,占用了负责打包的同事(其实是我自己)很多工作时间,所以最近一直在寻找加速打包的方案。目前的项目架构我们的项目使用CocoaPods来管理第三方库和私有库的依赖,这应该是大部分项目的标配。目前是纯Objective-C项目,没有引入Swift。调研过的解决方案下面列出了我调研过的一些主流方案以及最终没有采用的原因。这些解决方案都有其局限性,但也给了我很多启发。思考过程与最终解决方案一样有价值。cocoapods-packagercocoapods-packager可以将任意pod打包成一个静态库,节省了重复编译的时间,并且可以在一定程度上加快编译时间,但是它也有自己的缺点:优化不彻底,只有第三方和私有Pod的编译可以优化速度,对于其他频繁变化的业务代码,无计可施。私有库和第三方库的后续更新非常麻烦。修改源代码时,需要重新打包上传到内部Git仓库。二进制文件太多会减慢Git的运行速度(Git的LFS还没有部署)源码调试困难Carthage这个方案和cocoapods-packager类似,优缺点相似,但是Carthage可以更方便的调试源码.因为我们已经大规模使用了CocoaPods,切换到Carthage进行包管理需要做大量的转换工作,所以不考虑这个方案。BuckBuck是Facebook开源的通用构建系统。最大的特点是智能增量编译可以大大提高构建速度。当我第一次听说Buck时,它只能在Android上使用,但现在它可以兼容iOS。它可以加快构建速度的主要原因是缓存编译结果。通过持续监控工程目录下的文件变化,每次编译时只编译变化的文件。另一个启发我的功能是HTTPCacheServer,通过一个缓存文件服务器保存大家的编译结果,这样只要团队中有一个人编译过文件,其他人就不需要编译了,直接下载即可。Buck是一个比较完善的解决方案,很多国外的大公司比如Uber都已经在用了。我也花了很多时间研究,最后觉得不是很适合我们的项目和团队。主要原因是:Buck放弃了Xcode工程文件,需要手动编写配置文件指定编译规则,需要对现有工程进行大量调整。我们目前正在快速迭代新功能,没有时间和人力来实现它们。开发和调试过程都必须进行大量更改。因为Buck接管了项目编译的过程,所以如果你想调试项目,你不能直接在Xcode中按?+R。你必须先打开Buck来生成Xcode项目文件。Uber工程师甚至推荐使用Nuclide而不是Xcode作为开发环境。虽然原则上是可行的,但团队需要大量的时间去适应,短期的效率下降在所难免。使用Xcode调试代码无法享受编译速度更快的好处。虽然可以使用buck命令启动App,然后在命令行启动lldb进行调试,但是之后就不能使用Xcode的ViewDebugging、MemoryGraphDebugger等调试工具了。BazelBazel与Buck非常相似。它由谷歌开源。其优缺点与Buck类似,不再赘述。distcc分布式编译的原理是将一些需要编译的文件发送给服务器,编译完成后服务器将编译后的产物发回。尝试了大名鼎鼎的distcc,构建过程比较简单,最终能够成功的将编译任务分发到内网的多台服务器上。但是其他编译服务器的CPU使用率总是很低,只有20%左右;也就是说,派发任务的速度连服务器编译的速度都赶不上,派发任务和返回编译产物的过程要比本地直接编译耗时更多。.不断调整参数,尝试了很多次,最后发现编译时间一点也没有加快,甚至还慢了一点。使用分布式编译可能不适合我们当前项目的规模。最终方案:CCache看一下我对方案的要求:可以大大提高编译速度,至少要减少50%的编译时间。无需对项目进行重大调整,无需更改开发工具链。CCache是??一个可以将编译后的中间产物缓存起来的工具,在其他很多领域都有使用,但是在iOS世界的实践比较少。经过我的实践,它可以满足我之前的三个要求。最早意识到是找到这篇文章:UsingccacheforFunandProfit|PSPDFKit内幕如果不使用CocoaPods,参考上面的文章即可。因为CocoaPods需要做一些额外的调整,所以无论如何我都会解释一下。下面我们来谈谈如何将CCache应用到一个使用CocoaPods作为包管理工具的iOS项目中。安装步骤:注意:项目路径不能包含中文,否则会影响CCache的正常工作。安装CCache首先需要在电脑上安装Homebrew,这对于使用macOS的程序员来说应该是标配,略过。通过Homebrew安装CCache,在命令行执行$brewinstallccache命令安装成功。创建CCache编译脚本为了让CCache参与到整个编译过程中,我们需要使用CCache作为项目的C编译器。当CCache找不到编译缓存时,它会将编译指令传递给真正的编译器clang。新建一个名为ccache-clang的文件,内容是下面的脚本,放到你的工程中ccache-clang#!/bin/shifttype-pccache>/dev/null2>&1;thenexportCCACHE_MAXSIZE=10GexportCCACHE_CPP2=trueexportCCACHE_HARDLINK=trueexportCCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches#指定日志文件路径到桌面,后面检查集成问题会有用,集成成功后删除,否则会占用磁盘空间exportCCACHE_LOGFILE='~/Desktop/CCache.log'execccache/usr/bin/clang"$@"elseexecclang"$@"fi在命令行中,cd到ccache-clang文件所在目录,修改其对可执行文件的权限$chmod777ccache-clang如果你代码或者第三方库的代码使用到C++时,复制ccache-clang文件,重命名为ccache-clang++。clang对应的调用也应该改为clang++,否则CCache将无法应用到C++代码中。#!/bin/shifttype-pccache>/dev/null2>&1;thenexportCCACHE_MAXSIZE=10GexportCCACHE_CPP2=trueexportCCACHE_HARDLINK=trueexportCCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches#指定log文件路径到桌面,删除成功后,否则会占用大量的磁盘空间。exportCCACHE_LOGFILE='~/Desktop/CCache.log'execccache/usr/bin/clang++"$@"elseexecclang++"$@"fi完成后工程中应该有这两个文件Xcode工程调整在你的工程中定义CC常量构建设置(BuildSettings),添加一个常量CC,这个值会让Xcode在编译时使用执行路径的可执行文件作为C编译器。CC常量的值是$(SRCROOT)/ccache-clang。如果你的脚本没有放在项目的根目录下,请自行调整路径。如果项目一运行就报错,请检查路径是否填写错误。关闭ClangModules因为CCache不支持ClangModules,所以需要关闭EnableModules选项。在CocoaPods上如何处理这个问题将在后面讨论。EnableModules关闭后需要做的调整因为EnableModules关闭了,所有的@import语句都要删除,换成#import语法。例如,将@importUIKit替换为#import。之后,如果你使用其他系统框架,如AVFoundation、CoreLocation等,现在Xcode不会自动为你导入它们。您必须在项目Target的BuildPhrase->LinkBinaryWithLibraries中手动导入它们。尝试编译测试效果,然后在命令行输入cache-s可以看到类似下面的ccache运行统计信息:cachedirectory/Users/mac/.ccacheprimaryconfig/Users/mac/.ccache/ccache.confsecondaryconfig(readonly)/usr/local/Cellar/ccache/3.3.4_1/etc/ccache.confcachehit(direct)14378cachehit(preprocessed)1029cachemiss7875cachehitrate66.18%calledforlink61calledforpreprocessing48compilefailed2preprocessorerror4can'tuseprecompiledheader70unsupportedcompileroption2332noinputfile11cleanupsperformed0filesincache35495cachesize1.3GBmaxcachesize5.0GB如果成功接入,就能看见cachemiss不为0。因为第一次编译没有cache,肯定全是miss。然后第二次编译,如果可以看到缓存命中数开始飙升,恭喜,访问成功。CocoaPods处理如果你的项目没有使用CocoaPods进行包管理,那么你已经完全连接成功,不需要进行下面的操作。因为CocoaPods会将第三方库单独打包成一个StaticLibrary(或者DynamicFramework,如果使用use_frameworks!选项),CocoaPods生成的StaticLibrary也需要关闭EnableModules选项。但是由于CocoaPods在每次执行podupdate的时候都会重新生成Pods工程,所以如果在Xcode中直接修改Pods工程中的EnableModules选项,下次执行podupdate时就会改回来。我们需要在Podfile中添加如下代码,让生成的工程关闭EnableModules选项,同时添加CC参数,否则pod编译时无法使用CCache加速:post_installdo|installer_representation|installer_representation.pods_project.targets.eachdo|目标|target.build_configurations.eachdo|config|#关闭EnableModulesconfig.build_settings['CLANG_ENABLE_MODULES']='NO'#在生成的Pods工程文件中添加CC参数,根据自己的工程config.build_settings['修改路径的值CC']='$(PODS_ROOT)/../ccache-clang'endendend需要注意的是,如果你使用的Pod指的是系统框架,比如AFNetworking指的是SystemConfiguration,你需要在BuildPhraseofyourownproject->LinkBinaryWithLibraries,否则在编译时可能会收到类似Undefinedsymbolsxxxforarchitectureyyy的错误。有点回到原来时代的感觉,不过考虑到编译速度的巨大提升,这个价格还是可以接受的。集成排查主要看日志文件的输出和ccache-s命令的统计信息。如果在日志中看到unsupportedcompileroption-fmodules之类的字样,说明你的EnableModules没有关闭。按照前面的步骤仔细检查。其他问题请参考官方文档中的Troubleshooting。进一步优化去除PrecompiledHeaderFilePCH的内容会附加在每个文件的前面,CCache根据文件内容的MD4摘要寻找缓存,所以当你修改PCH的内容或引用的头文件时PCH,会导致所有缓存失效,只能整体重新编译。CCache在第一次编译时需要更新缓存,会导致编译时间变长,对于北聊项目来说几乎翻倍。因此,如果PCH或者PCH导入的文件被频繁修改,缓存就会频繁丢失。在这种情况下,最好不要使用CCache。为了避免出现上述情况,我建议在PCH中尽量少引入头文件,只保留改动较少的系统框架和第三方类库的头文件。最好完全删除PCH。反正苹果现在不推荐使用PCH。Xcode创建的新项目默认没有PCH。尝试了团队内部共享缓存文件夹的优化方式,但最终效果不是很好,所以没有使用。在CCache官方文档中,有关于共享缓存文件夹的说明,其中描述了如何修改CCache的配置,使得编译缓存可以在多台计算机之间共享。理论上,只要其中一个人有编译文件,其他人就可以直接下载到,节省了整个团队的时间。因为Buck也有类似的机制,觉得值得一试,于是在公司局域网内架设了一个OwnCloud网盘,让大家共享自己电脑上的CCache缓存目录。虽然实验成功了,但实际效果并不好。因为在多台计算机上同步几个G大小的缓存目录需要在后台进行大量的文件比较和传输工作,在编译的同时进行这些操作会消耗大量的计算资源,反而会减慢编译速度。另外去掉PCH后,缓存命中率其实还是挺可观的,没有必要通过共享缓存来进一步提升缓存命中率,所以最终还是放弃了共享缓存的想法。如果对缓存命中率还是不满意,可以考虑尝试这个方向。总结通过CCache的集成,我们的项目在Xcode中的打包时间(在菜单中选择Product->Archive)从11到12分钟减少到130秒,大约增加了五倍,结果是可喜。集成过程其实非常简单。从开始到集成成功,我一共花了两个小时。如果你也为编译时间长而苦恼,建议试一试。
