前言

贝聊目前开发的两款App分别是贝聊家长版和贝聊老师版,最近因为在快速迭代开发新功能,项目规模急速增长,单个端业务代码约23万行,私有库约6万行,第三方库代码约15万行,单个客户端的代码行数约60万。现在打包一次耗时需要11~12分钟。虽然还远远比不上 Facebook 的40分钟,但是我们在内测的时候,经常一天要发布内测版两到三次。打包时CPU占用基本上是百分百的,因为没有专门的 CI 机器,对负责打包的同事(其实就是我自己)的工作时间占用比较多,所以最近一直在寻找加快打包速度的方案。

目前的项目架构

我们的项目使用 CocoaPods 来管理第三方库和私有库的依赖,对大部分项目来说应该是标配了。目前还是纯 Objective-C 的项目,没有引入 Swift。

调研过的方案

下面列出我研究过的一些主流方案以及我最后没有采用的原因,这些方案有各自的局限性,但是也给了我不少启发,思考过程跟最终方案一样有价值。

cocoapods-packager

cocoapods-packager 可以将任意的 pod 打包成 Static Library,省去重复编译的时间,一定程度上可以加快编译时间,但是也有自身的缺点:

  1. 优化不彻底,只能优化第三方和私有 Pod 的编译速度,对于其他改动频繁的业务代码无能为力
  2. 私有库和第三方库的后续更新很麻烦,当有源码修改后,需要重新打包上传到内部的 Git 仓库
  3. 过多的二进制文件会拖慢 Git 的操作速度(目前还没部署 Git 的 LFS)
  4. 难以调试源码

Carthage

这个方案跟 cocoapods-packager 比较类似,优缺点都差不多,但 Carthage 可以比较方便地调试源码。因为我们目前已经大规模使用 CocoaPods,转用 Carthage 来做包管理需要做大量的转换工作,所以不考虑这个方案了。

Buck

Buck 是一套通用的构建系统,由 Facebook 开源。最大的特色是智能的增量编译可以极大地提高构建速度。最早听说 Buck 的时候,它还只能用在安卓上,现在已经适配了 iOS。

它能增快构建速度的主要原因是缓存了编译结果,通过持续监视项目目录的文件变化,每次编译时只编译有改动的文件。另外一个让我很受启发的功能是 HTTP Cache Server,通过一台缓存文件服务器来保存大家的编译结果,这样只要团队里其中一人编译过的文件,其他人就不用再编译了,直接下载就行。

Buck 是个相当完备的解决方案,很多国外的大公司例如 Uber 都已经用上。我也花了很多时间来研究,最终还是认为对我们的项目和团队来说,目前并不是很适合,主要原因是:

  1. Buck 抛弃了 Xcode 的项目文件,需要手工编写配置文件来指定编译规则,这要对现有项目作出大幅度的调整。我们目前还在快速迭代新功能,没有余暇和人手来实施。
  2. 开发和调试的流程都得做出很大的改变。因为 Buck 接管了项目编译的过程,想调试项目不能简单地在 Xcode 里面 ?+R 了,得先反过来让 Buck 生成 Xcode 的项目文件。Uber 的工程师甚至推荐使用 Nuclide 来代替 Xcode 作为开发环境。虽然原理上是可行的,但是团队需要花不少时间来适应,短期内效率降低无可避免。
  3. 用 Xcode 调试代码享受不到加快编译速度的好处。虽然可以用 buck 命令启动 App,然后在命令行里启动 lldb 来调试,但那就无法使用 Xcode 的调试工具 例如 View Debugging 和 Memory Graph Debugger。

Bazel

Bazel 跟 Buck 很相似,是 Google 开源的,优缺点跟 Buck 都差不多,不再详细说了。

distcc 分布式编译

原理是把一部分需要编译的文件发送到服务器上,服务器编译完成后把编译产物传回来。我尝试了一下比较出名的 distcc,搭建过程比较简单,最后也能成功地把编译任务分派到内网的多台服务器上。但是其他编译服务器的 CPU 占用总是很低,只有 20% 左右;也就是说分派任务的速度甚至还赶不上服务器编译的速度,分派任务然后回传编译产物这个过程所耗费的时间超过了本地直接编译。不停调整参数反复试验了很多次,最后发现编译时间完全没有变快,甚至还有点变慢了。可能以我们目前项目的规模并不适合使用分布式编译。

最终方案:CCache

先来看看我对于解决方案的诉求:

  1. 能大幅度地提升编译速度,起码要减少掉 50% 的编译时间
  2. 不需要对项目作出重大调整
  3. 不需要改变开发工具链

CCache 是一个能够把编译的中间产物缓存起来的工具,在其他领域已经有不少应用,只是在 iOS 界的实践比较少。经过我的实践,它能够满足我前面的三点要求。我最早认识到它是搜到了这篇文章:Using ccache for Fun and Profit | Inside PSPDFKit

如果你不使用 CocoaPods,参照上面的文章即可。因为针对 CocoaPods 需要作出一些额外的调整,所以还是说明一下。下面就来说说要怎样把 CCache 应用在用 CocoaPods 作为包管理工具的 iOS 项目中。

安装步骤:

注意:项目路径不能有中文,否则会影响 CCache 的正常工作

安装 CCache

首先你需要在电脑上安装 Homebrew,对使用 macOS 的程序员来说应该是标配,略过。

通过 Homebrew 安装 CCache, 在命令行中执行

$ brew install ccache

命令跑完后即安装成功。

创建 CCache 编译脚本

为了能让 CCache 介入到整个编译的过程,我们要把 CCache 作为项目的 C 编译器,当 CCache 找不到编译缓存时,它会再把编译指令传递给真正的编译器 clang。

新建一个文件命名为ccache-clang, 内容为下面这段脚本,放到你的项目里

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++ 的代码上。

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

完成后项目中应该有这两个文件

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。之后,如果你用到了其他的系统框架例如 AVFoundation、CoreLocation等,现在 Xcode 不会再帮你自动引入了,你得要在项目 Target 的 Build Phrase -> Link Binary With Libraries 里面自己手动引入。

测试效果

尝试编译一遍,然后在命令行里输入 cache -s 就能看见类似下面的 ccache 运行情况统计:

cache directory                     /Users/mac/.ccache

primary config                      /Users/mac/.ccache/ccache.conf

secondary config      (readonly)    /usr/local/Cellar/ccache/3.3.4_1/etc/ccache.conf

cache hit (direct)                 14378

cache hit (preprocessed)            1029

cache miss                          7875

cache hit rate                     66.18 %

called for link                       61

called for preprocessing              48

compile failed                         2

preprocessor error                     4

can’t use precompiled header          70

unsupported compiler option         2332

no input file                         11

cleanups performed                     0

files in cache                     35495

cache size                           1.3 GB

max cache size                       5.0 GB

如果成功接入,就能看见 cache miss 不为0。因为第一次编译没有缓存,肯定是全 miss 的。接着编译第二遍,如果能看见 cache hit 的数字开始飙升,恭喜你,接入成功了。

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一类的错误。有点回到了原始时代的感觉,但考虑到编译速度的极大提升,这一点代价可以接受。

集成问题排查

重点关注日志文件的输出和ccache -s 命令的统计,如果在日志中看到了 unsupported compiler option -fmodules 这样的字眼,就是你的 Enable Modules 没有关掉了,根据前面的步骤仔细检查。其他问题,参考官方文档的 Troubleshooting。

进一步的优化

移除 Precompiled Header File

PCH 的内容会被附加在每个文件前面,而 CCache 是根据文件内容的 MD4 摘要来查找缓存的,因此当你修改了 PCH 或者 PCH 引用到的头文件的内容时,会造成全部缓存失效,只能全体重新编译。CCache 在首次编译的时候因为需要更新缓存,会造成编译时间变长,对贝聊的项目来说变长了差不多一倍。因此如果 PCH 或者 PCH 引入的文件被频繁修改的话,缓存就会频繁地 miss,这种情况下还不如不用 CCache。

为了避免以上这种情况,我建议在 PCH 里面尽量少引入头文件,只保留比较少更改的系统框架和第三方类库的头文件。最好是把 PCH 彻底删除,反正苹果现在也不建议使用 PCH 了,Xcode 新建的项目默认都是不带 PCH 的。

在团队内部共享缓存文件夹

这个优化方式我尝试过,最终效果不是很好,因此没有采用。CCache 的官方文档中有一段关于共享缓存文件夹的说明,描述了如何修改 CCache 的配置,让编译缓存能够在多台电脑之间公用,理论上只要其中一个人编译过的文件其他人就能直接下载到了,节约了整个团队的时间。因为 Buck 也有类似的机制,我觉得值得尝试一下,便在公司局域网内搭建了一个 OwnCloud 网盘,让大家把自己电脑上的 CCache 缓存目录放上去共享。虽然试验是成功了,但是实际效果并不好。因为同步在多台电脑上大小达到几个G的缓存目录,需要在后台进行很多文件的对比和传输的工作,在编译的同时进行这些操作会耗费不少计算资源,反而会拖慢编译速度。加上移除掉 PCH 后,其实缓存的命中率已经相当可观了,不太需要通过共享缓存来进一步提高缓存命中率,所以我最后放弃了共享缓存这个想法。如果你对缓存命中率还是不满意的话,可以考虑往这个方向尝试一下。

总结

通过集成 CCache,我们的项目在 Xcode 里面的打包(在菜单里面选择 Product -> Archive)时间从 11~12分钟减少到了 130 秒,大概有五倍的提升,成果喜人。集成的过程其实很简单,我从开始尝试到集成成功总共就花了两个小时。如果你也被过长的编译时间困扰,建议尝试一下。

iOS - Xcode提高编译速度,增量编译,ccache相关推荐

  1. 微信团队分享:极致优化,iOS版微信编译速度3倍提升的实践总结

    1.引言 岁月真是个养猪场,这几年,人胖了,微信代码也翻了. 记得 14 年转岗来微信时,用自己笔记本编译微信工程才十来分钟.如今用公司配的 17 年款 27-inch iMac 编译要接近半小时:偶 ...

  2. 优化XCode的编译速度

    1.将Debug Information Format改为DWARF 在工程对应Target的Build Settings中,找到Debug Information Format这一项,将Debug时 ...

  3. HighTec编译器提高编译速度

    当我们工程特别大时(尤其是基于AUTOSAR开发的工程,包含了BSW协议栈及MCAL,ASW的代码其c文件数量级可达上千)全编译一个工程需要很长时间. HighTec是增量式编译,每次只重新编译修改的 ...

  4. [贝聊科技]如何将 iOS 项目的编译速度提高5倍

    前言 贝聊目前开发的两款App分别是贝聊家长版和贝聊老师版,最近因为在快速迭代开发新功能,项目规模急速增长,单个端业务代码约23万行,私有库约6万行,第三方库代码约15万行,单个客户端的代码行数约60 ...

  5. 如何提高 Xcode 的编译速度

    本文总结自 WWDC 2018 building faster in xcode 该 Session 通过一系列的实践来实现 Xcode 的快速编译,共阐述了六个大方面,分别是: 将编译过程并行化 通 ...

  6. ios系统脚本服务器加速,提高iOS项目的编译速度

    项目结构 CocoaPods做业务划分,每条业务线一个工程,每个工程依赖基础框架,业务线之间解耦依赖基础模块 纯Objective-C代码,没有引入Swift代码 独立服务器安装Xcode通过Jenk ...

  7. 提高编译速度的方法——ccache的使用

    在使用CMake编译大型工程时.如果每次只是单独对一个文件进行修改还好,CMake可以针对修改单独编译,但往往只要对CMakeLists做一点小修改,整个工程就要重新编译,对于普通性能的电脑来说通常非 ...

  8. Win10 WSL编译Android开启ccache(提高编译速度)

    当你删掉out/target目录或者使用make clean清空输出重新编译源码的时候,编译时间通常都很漫长. 其实这个问题很容易解决,Android官方为我们带来了解决方案–ccache编译器缓存. ...

  9. 使用预编译头提高编译速度

    什么是预编译头 在介绍预编译头之前,有必要了解一下C/C++的编译方式.C/C++的编译单元是源文件(带有.c..cc..cpp等扩展名的文件),在编译一个源文件之前,预处理器会把这个源文件中所有通过 ...

最新文章

  1. STM32F103的11个定时器详解
  2. log4j 源码解析_Log4j配置详解
  3. 在天气预报中应用机器学习
  4. 计算机网络部分(共44题),全国自学考试自考04741计算机网络原理4月考试真题
  5. 理解java虚拟机工作后了解吗_JAVA入门到再次入门——深入理解JAVA虚拟机(二)|七日打卡...
  6. IDC机房对接阿里云
  7. 如何用轻量协作工具做bug管理
  8. tmux手册中文翻译
  9. Android学习二:Http操作
  10. 彩电主板更换后图像是矩型怎么处理_液晶彩电主板常见故障维修
  11. IPv6 to IPv4过渡技术——手工隧道和GRE隧道配置实例
  12. Java 12网络编程
  13. java--GUI窗口可视化编程1
  14. su室外渲染参数设置_vray3.4 for sketchup渲染器渲染出图参数怎么设置?
  15. 【arp】关于arp和arping命令的使用
  16. h5难做吗_H5系列课程| 做一只涨工资的H5 没有你想得那么难
  17. 【玩转嵌入式屏幕显示】(六)ST7789 SPI LCD硬件垂直滚动功能的使用
  18. window操作系统快捷键
  19. cubieboard
  20. 虚拟机安装python3_虚拟机如何安装python

热门文章

  1. 谈移动互联网开发的数据分析和决策思路
  2. 奥运会运动项目名称汇总
  3. 小萌库- 新海诚那些唯美感人的动漫
  4. Jetson 基本笔录
  5. VH6501模板工程介绍(一)
  6. unity(登录注册用手机号短信验证)
  7. 协变 (Covariant)、逆变 (Contravariant) 与不变 (Invariant)
  8. Java缓存知识汇总
  9. Windows系统忘记开机密码解决方案
  10. A Native Collection has not been disposed, resulting in a memory leak. Enable Full StackTraces to ge