[iOS]ccache 让你的编译时间飞起来

2017-05-31 by NewPan






原文地址:Using ccache for Fun and Profit
作者 Peter Steinberger


我们的 PSPDFKit 项目超过 60 万行代码,并且代码量还在增长。尽管我们致力于写简洁而高效的代码,但是这个项目很大,而且有许多边界情况需要尤其注意。在 PSPDFKit 5 for iOS 项目上,编译时间尤其成为一个令人头痛的问题:每次编译都很慢。


我们的安卓 SDK 也有同样的问题,几个月前我们的安卓负责人在技术栈中引入了 ccache 来处理冗长的 C++ NDK 编译时间,我也是从那个时候开始接触 ccache。



ccache 是个啥?

ccache 是一个编译缓存器,它会在实际编译之前先检查缓存。它有直接和预处理模式,而且由于在 Clang 3.2 版本之前是不支持 ccache 插件,所以在 Clang 3.2 之前会有一些问题,但是现在 Clang 的版本是 3.2.3,所以没有 Clang 不支持的问题。ccache 是一个具有悠久历史的项目,其主要焦点是快速正确。


网上搜到“ccache xcode”的信息都是过时无效的信息,经过我快速的尝试网上的方法,都无法配置好使其正常工作。随着我们的代码库越来越复杂,同时我们的 Jenkins 工作集群数也有 10 台 Mac,现在测试时间从几乎无法忍受变成了正真无法忍受。在 Twitter 抱怨现在每天的工作就是管理 Jenkins 工作集群之后,Facebook 的 Christian Legnitto(他之前在 Apple 负责 OS X 版本管理工作)建议我们尝试 ccache。


Let’s get started

使用以下命令安装 ccache :


brew install ccache

如果你没安装 Homebrew,请移步这里,先去安装 Homebrew,如果你不想移步,就直接使用以下命令安装 Homebrew:


/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

为了让 Xcode 调用 ccache,我们需要一个小脚本来配置一些环境变量,然后再调用 ccache。将这个脚本保存到您项目的某个地方,并将其命名为 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
exec ccache /usr/bin/clang "$@"
else
exec clang "$@"
fi

根据你的具体情况,如果你的项目中有 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
exec ccache /usr/bin/clang "$@"
else
exec clang++ "$@"
fi

这样看起来是不是有点复杂,如果没有命中缓存,那么将会按照之前的编译方式一样编译,而不是报 ccache not found(找不到缓存)的错误(ccache 内置 shell 脚本,所以检查缓存很迅速)。


创建 shell 脚本方法:


创建 touch ccache-clang
打开脚本 open -a xcode ccache-clang
粘贴脚本内容
执行脚本 chmod 755 ccache-clang

如果去学习 ccache 的配置,你会发现有很多选项可选。上面我们使用的是一种相当激进的缓存策略,同时运行良好。对于你自己的项目,你可能在没有 CCACHE_SLOPPINESS 的情况下开始,然后在一切运行良好的情况下一次性添加缓存。


这里最重要的参数是 CCACHE_CPP2,这个参数用于解决 Clang 将处理预处理器的文件输出,并可能会发现许多你没有注意到的潜在问题,例如由于宏扩展导致的不必要的括号。使用此选项会稍微减慢编译时间,但是要比完全没有使用 ccache 要快得多。Peter Eisentraut 写了一篇关于这个问题的好文章。


您还需要在 Xcode 中定义 CC 变量。在 PSPDFKit 中,我们在 .xcconfig 文件中执行此操作,这个文件在我们所有项目中共享(这是一个很好的统一的项目配置,和易于阅读和查找)。同时,您可以直接在 Xcode 项目设置内配置:


CC = "$(SRCROOT)/../Resources/ccache-clang"




就这么多了!下次编译的时候会比正常慢一点,你可以在终端中使用 ccache -s 来查看 ccache 是否正常工作。刚开始时应该有很多缓存没有命中,但是当缓存开始渐渐替代之后的编译时,编译速度将会变得快起来。


坑来了

路不平的地方就有坑:ccache 有一些缺点。


不支持 Clang 的 modules,如果检测到 -fmodules, ccache 就会失效。因此,为了兼容 ccache,你需要用老旧的 # import <UIKit/UIKit.h> 替换你项目中所有优雅的 @import UIKit,以及所有使用 ccache 带来的问题,比方说宏的问题。在 PSPDFKit 项目中我们采用了 Objective-C++ 的形式,当我们使用很多 C++ 代码时,就无法使用 modules 了,所以这一点(ccache 不支持 modules)并没有影响到我们。 modules 会自动链接用到的 framework,但是在禁用了 modules 以后,你需要手动添加用到的 framework,这个工作很无趣,但是也很快就做完。


还需要停止使用 .pch。苹果不推荐使用 .pch,而且一般认为使用 .pch 是不好的编程风格,哪里用到就在哪里导入会比 .pch 要好。对我们而言,删除那些 .pch 还是很容易的。当然,ccache 没法帮你缓存 Swift 文件。虽然 Swift 也使用 Clang,但是ccache 对 Swift 文件束手无策。也许 ccache 最终会支持 Swift,但我指望不上。因为 Swift 至今没有稳定,甚至我们要在 Swift 的两个版本之间做二进制兼容,我们没法用 Swift 来编写我们的 SDK,所以 ccache 不支持 Swift 的问题,对我们不是问题。


在编译期间,我们应该随时监视项目是否抛出不兼容的警告。请参阅“不支持的编译器”选项。我花了相当一部分时间去处理这些不兼容的问题。设置 CCACHE_LOGFILE 临时环境变量将有助于我们精确定位错误:ccache 将会提示那些标识是有问题的,以及缓存命中和未命中的具体情况。


steipete@steipete-rmbp ~ $ ccache -s
cache directory /Users/steipete/.ccache
primary config /Users/steipete/.ccache/ccache.conf
secondary config (readonly) /usr/local/Cellar/ccache/3.2.3/etc/ccache.conf
cache hit (direct) 42530
cache hit (preprocessed) 18147
cache miss 28379
called for link 1344
called for preprocessing 645
compile failed 1
preprocessor error 2
can't use precompiled header 2567
unsupported source language 12
unsupported compiler option 11564
no input file 2
files in cache 124223
cache size 8.7 GB
max cache size 15.0 GB
搞这个值不值?

给你说一下我们使用的情况,使用了 ccache 以后,我们的编译运行时间平均为 8 分钟,之前我们没有用 ccache 的时候是 14 分钟。使用 ccache 之前在最快的 MacBook Pro 上编译打包整个 PSPDFKit 需要 50 分钟,使用了之后,时间为 15 分钟。添加 ccache 到我们的技术栈是一个巨大的进步,真后悔我没有早点知道这个那么棒的工具!


Precompiled Header 问题

Anton Bukov 说通过禁用 GCC_PRECOMPILE_PREFIX_HEADER,开启 GCC_PREFIX_HEADER 的方式来处理这个问题。


我的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。如果某篇文章刚好在你的实际开发中帮到你,又或者提供一种不同的实现思路,让你觉得有用,那就看看这句话 “坚持每天点赞的人,99%都是帅哥美女,再也不用单身了”。


我的文章集合索引


你还可以关注我自己维护的简书专题 iOS开发心得。这个专题的文章都是实打实的干货。
如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy上给我留言,以及访问我的 Github。


[iOS]ccache 让你的编译时间飞起来 - IOS - 第七城市

第七城市