11、编译缓存与增量编译:ccache 配置、ninja 增量编译原理、清理编译产物

说到 Android 编译,我猜你肯定遇到过这种情况:改了一行 Java 代码,结果重新编译等了半小时。嗯,这其实不是你的错,而是构建系统在「重新发明轮子」。今天我们就来聊聊怎么让编译变快——说白了,就是让机器少干点重复劳动。

为什么需要编译缓存?

先想一个问题:你编译一次完整的 Android 系统,可能要生成几万个 .o 文件。如果你只改了一个 .c 文件,理论上只需要重新编译这一个文件,然后重新链接就行。但现实是,很多构建系统会「过度编译」——它不确定哪些文件受影响,干脆全部重来。

我刚开始做 Android 系统开发时,就吃过这个亏。有一次我改了一个头文件,结果整个 framework 层重新编译了 40 分钟。后来才发现,是 ccache 没配好,缓存根本没命中。

核心思想:编译缓存的核心就是「以空间换时间」。把编译结果存起来,下次遇到同样的输入(源文件 + 编译选项),直接返回缓存结果,跳过真正的编译过程。

ccache 配置实战

ccache 是 C/C++ 编译器的缓存工具。在 Android 构建中,它主要缓存 .o 文件的编译结果。配置其实很简单,但有几个坑要注意。

1. 安装 ccache

# Ubuntu/Debian
sudo apt-get install ccache

# macOS
brew install ccache

2. 配置环境变量

我个人习惯在 ~/.bashrc~/.zshrc 里加上这几行:

export USE_CCACHE=1
export CCACHE_DIR=/home/yourname/.ccache
export CCACHE_MAXSIZE=50G
export CCACHE_COMPRESS=1
export CCACHE_COMPRESSLEVEL=6

这里解释一下每个参数:

参数 说明 我推荐的值
CCACHE_DIR 缓存存放路径 建议放在 SSD 上
CCACHE_MAXSIZE 缓存最大容量 50G ~ 100G
CCACHE_COMPRESS 是否压缩缓存 1(开启)
CCACHE_COMPRESSLEVEL 压缩级别 6(平衡速度和大小)

小技巧:缓存目录一定要放在 SSD 上。我曾经放在 HDD 上,结果缓存读写比编译还慢,得不偿失。

3. 初始化缓存

# 设置缓存大小
ccache -M 50G

# 查看缓存状态
ccache -s

运行 ccache -s 后,你会看到类似这样的输出:

cache directory                     /home/yourname/.ccache
primary config                      /home/yourname/.ccache/ccache.conf
secondary config      (readonly)    /etc/ccache.conf
cache hit (direct)                  12345
cache hit (preprocessed)            6789
cache miss                          2345
cache hit rate                      89.2 %
...

看到 cache hit rate 在 80% 以上,说明配置生效了。

ninja 增量编译原理

ccache 解决的是「单个文件编译」的缓存问题。而 ninja 解决的是「哪些文件需要重新编译」的问题。两者配合,才能达到最快的增量编译速度。

ninja 的增量编译原理,说白了就是「依赖图 + 时间戳」。它维护一个 .ninja_log 文件,记录每个目标的编译时间和依赖关系。当你再次编译时,ninja 会做三件事:

  1. 检查依赖变化:比较源文件和目标文件的时间戳
  2. 构建依赖图:找出所有受影响的节点
  3. 只编译变化的部分:跳过未变化的节点

举个例子,假设你有这样的依赖关系:

app.o ← app.c ← app.h
        ← utils.h

如果你改了 app.c,ninja 只会重新编译 app.o。但如果你改了 app.h,所有包含这个头文件的 .c 文件都会被重新编译。

注意:ninja 的增量编译依赖时间戳。如果你用 git checkout 切换分支,文件时间戳可能会被重置,导致 ninja 认为所有文件都变了。这时候建议先 touch 一下关键文件,或者直接 make clean 重新编译。

清理编译产物

编译产物多了,磁盘空间会告急。而且有时候缓存「脏」了,需要彻底清理。我总结了几种清理方式:

1. 轻度清理:只清理临时文件

# 清理 .o 文件,保留 .so 和 .apk
make clean

# 或者用 ninja
ninja -t clean

2. 中度清理:清理所有编译产物

# 回到初始状态
make clobber

# 或者直接删掉 out 目录
rm -rf out/

3. 重度清理:连 ccache 一起清

# 清空 ccache
ccache -C

# 查看清理后的缓存状态
ccache -s

我的经验:不要频繁 make clobber。我见过有人每次编译前都 clean 一次,结果每次都是全量编译,白白浪费半小时。只有在遇到诡异的编译错误时,才考虑彻底清理。

知识体系总览

下面这张图总结了本章的核心逻辑:

编译缓存与增量编译知识体系 ccache 缓存 • 缓存 .o 文件 • 基于输入哈希 • 支持压缩存储 • 命中率 > 80% • 配置:CCACHE_DIR • 限制:CCACHE_MAXSIZE • 压缩:CCACHE_COMPRESS ninja 增量编译 • 依赖图构建 • 时间戳比较 • 只编译变化部分 • .ninja_log 记录 • 依赖:头文件追踪 • 并行:多任务执行 • 缓存:ccache 配合 清理编译产物 • make clean • make clobber • rm -rf out/ • ccache -C • 轻度:清 .o 文件 • 中度:清所有产物 • 重度:清缓存 提供缓存 产物管理 三者配合:ccache 缓存编译结果 → ninja 决定编译范围 → 清理释放空间

避坑指南

最后分享几个我踩过的坑:

  • ccache 缓存太大导致磁盘满:我曾经设了 100G 缓存,结果忘了限制,最后磁盘爆了。建议设 50G 上限,定期用 ccache -s 检查。
  • ninja 增量编译失效:如果你发现改了代码但 ninja 不重新编译,可能是时间戳没更新。试试 touch 一下源文件。
  • 清理后编译变慢:清理 ccache 后,第一次编译会变慢。这是正常的,因为缓存需要重新建立。

嗯,关于编译缓存和增量编译,今天就聊这么多。记住一个原则:能不编译就不编译,能复用就复用。这样你的开发效率至少能提升一倍。


公众号:蓝海资料掘金营,微信deep3321