启用和配置AGP构建缓存

构建缓存这东西,说白了就是让Gradle记住你上一次编译产生的中产物。下次再编译时,如果输入没变,直接从缓存里拿,省去重新编译的时间。我当年刚接触Android构建时,看着每次clean build那漫长的等待,就琢磨着肯定有办法加速。后来发现AGP(Android Gradle Plugin)自带构建缓存机制,配置得当的话,增量编译速度能翻一倍。

但要注意——默认情况下,构建缓存并没有完全开启。你需要手动配置,而且配置的方式直接影响效果。踩过坑的人都知道,配不好反而会引入缓存冲突,导致诡异的问题。

一、构建缓存的核心概念

AGP的构建缓存,本质上是任务级别的输出缓存。每个Gradle Task(比如编译Java/Kotlin、处理Android资源、生成Dex等)执行后,会将输出产物(.class文件、.dex文件、压缩的资源包等)按照输入内容的哈希值存入缓存。下次再跑同一个Task,如果输入没变,直接复用缓存。

术语 含义
缓存键(Cache Key) 由Task的输入文件、参数、依赖等计算出的唯一哈希值
缓存命中(Cache Hit) 当前Task的缓存键已存在于缓存中,直接复用上一次的输出
缓存未命中(Cache Miss) 缓存键不存在,需要重新执行Task,并将结果存入缓存
缓存目录 存放缓存产物的本地文件夹,默认在 ~/.gradle/caches/

我个人习惯把缓存想象成一个“任务快照仓库”。你改了一行Java代码,只有涉及这个文件的Task会重新编译,其他Task直接从仓库里拿现成的结果。嗯,这里要注意:并不是所有Task都支持缓存,比如某些自定义Task如果没标注@CacheableTask,就不会被缓存。

小提示:你在build.gradle里声明的transform或自定义插件,如果想让它们支持缓存,需要添加@CacheableTask注解,并且正确声明输入输出。否则会被AGP跳过,白忙一场。

二、如何启用构建缓存

启用方式很简单,就一行配置。但我建议你根据自己的项目情况选择路径。

方式一:全局配置(gradle.properties)

在项目根目录下的 gradle.properties 文件中添加:

org.gradle.caching=true

这会启用Gradle的通用构建缓存。但你想想看,AGP在7.0之后有自己的缓存策略,所以还需要额外开启AGP的缓存:

android.enableBuildCache=true

两者结合,才能让Android相关的Task也受益。

方式二:按模块配置(build.gradle)

如果你只想针对某个模块启用,可以在模块级别的build.gradle里写:

android {
    buildCache {
        local {
            enabled = true
        }
    }
}

我见过一些大项目,因为各模块依赖复杂,全局启用缓存后反而导致“哈希冲突”或“缓存污染”。这时候按模块细粒度控制更稳妥。

避坑指南:我曾经在一个多模块项目中全局开启了构建缓存,结果改了A模块,B模块却一直用旧缓存,导致运行时奔溃。排查了很久才发现,是因为A模块和B模块共享了部分资源,缓存键计算时没有考虑到这种隐式依赖。后来我改用buildCache.local.enabled = true并配合cacheConfiguration指定了缓存目录,问题才解决。

三、配置缓存目录与大小

默认缓存目录在 ~/.gradle/caches/ 下,但你可以通过下面的配置自定义:

android {
    buildCache {
        local {
            directory = new File(rootDir, "build-cache")
            removeUnusedEntriesAfterDays = 7
            // 以下3个参数只有Gradle 7.0+支持
            targetSizeInMB = 1024
            maxSizeInMB = 2048
        }
    }
}
  • directory:指定缓存目录,推荐放在项目目录内(方便清除),或者放在SSD上提升I/O速度。
  • removeUnusedEntriesAfterDays:自动清理超过多少天未使用的缓存,默认7天。我习惯设为3天,因为迭代快,旧缓存意义不大。
  • targetSizeInMB / maxSizeInMB:控制缓存上限。我曾遇到过CI服务器磁盘爆满,就是因为缓存积累太多没清理。务必设置上限。
重要:如果你使用远程缓存(比如公司内部的Gradle缓存服务器),需要额外配置remote区块和认证信息。但本地开发建议优先把本地缓存配好,远程缓存更适合CI/CD环境。

四、缓存命中率的验证手段

配置完了,你得验证到底有没有生效。我常用的招数有两个:

1. 通过Gradle命令的--info日志

./gradlew assembleDebug --info | grep "CACHED"

如果输出中出现 Task :xxx:compileDebugKotlin FROM CACHE 或 Task :xxx:mergeDebugResources FROM CACHE,说明缓存命中了。

2. 利用Build Analyzer插件

Android Studio自带的Build Analyzer(在Build菜单下)能展示每个Task的执行时间、缓存命中情况。这是我个人最推荐的诊断工具——可视化的,一看就懂。

我曾经在一个20+模块的项目里,通过Build Analyzer发现某个资源处理Task始终没命中缓存。排查后发现是因为配置了aaptOptions.additionalParameters,这个参数没被包含在缓存键里,导致每次都被当作不同的输入。修正配置后,缓存命中率从30%飙升到85%。

经验之谈:缓存命中率低于50%的时候,建议你检查一下是否有动态版本依赖(如1.+)、是否使用了--rerun-tasks或者clean命令频繁。另外,BuildConfig中的时间戳也可能导致缓存失效,建议在buildConfigField中避免使用System.currentTimeMillis()。

五、构建缓存工作流程图

下面这张图展示了AGP构建缓存的核心流程:从源文件变化到最终复用缓存的决策过程。

AGP构建缓存工作流程 开发修改源码 (Java/Kotlin/资源) Gradle Task执行 (如:compileDebugKotlin) 计算缓存键 (输入文件哈希) 查询本地缓存 (~/.gradle/caches) 缓存未命中 重新编译并存储 说明:若缓存命中,Task直接跳过执行,仅几毫秒的I/O开销即完成。 若未命中,则执行原始编译流程并将产物写入缓存,供下次使用。 虚线表示“未命中”后重新编译并存储,而非直接返回。

六、高级配置:远程缓存(CI/CD场景)

如果你的项目使用了持续集成(CI),本地缓存每次构建都会从零开始。这时候远程缓存就派上用场了。配置如下:

// 在项目根目录 build.gradle 或 settings.gradle 中
buildCache {
    remote(HttpBuildCache) {
        url = 'https://build-cache.yourcompany.com/cache/'
        allowUntrustedServer = false
        // 如果需要认证
        credentials {
            username = 'cache-user'
            password = 'cache-password'
        }
    }
}

要注意:远程缓存服务器需要支持 Gradle Build Cache Node 或兼容的HTTP API。我建议团队里统一部署一个缓存节点,构建速度能提升40%以上。

避坑指南:我曾见过团队配置了远程缓存,但忘了用--build-cache参数执行Gradle,导致每次CI都是冷启动。还有一个坑:远程缓存和本地缓存的优先级问题。默认情况下,Gradle先查本地再查远程。如果你本地缓存太大、I/O慢,建议调小本地缓存上限,让远程缓存发挥更大作用。具体做法是设置local.enabled = false,只使用远程缓存。

七、清理缓存的最佳实践

缓存不是越多越好。遇到以下情况时,你需要手动清理:

  • 升级Gradle或AGP版本后:缓存格式可能变更,旧缓存无法使用,反而占用空间。建议执行./gradlew cleanBuildCache。
  • 遇到奇怪的编译错误,而别人能编译过:大概率是本地缓存污染。先执行./gradlew cleanBuildCache再rebuild。
  • 磁盘空间告急:直接删除 ~/.gradle/caches/build-cache-* 目录,或者用 ./gradlew --stop 停止守护进程后手动删除。

我个人习惯在团队内推行每周一清理一次本地缓存。因为周末没人构建,周一第一次构建可以容忍慢一些,之后缓存就是热乎的了。有效避免了“缓存腐化”引发的奇怪问题。

最后总结几条铁律:
1. 始终在 gradle.properties 中开启 org.gradle.caching=true 和 android.enableBuildCache=true。
2. 设定缓存大小上限,避免撑爆磁盘。
3. 修改Task输入后,注意检查缓存键是否覆盖所有依赖。
4. CI环境中优先使用远程缓存,并设置--build-cache参数开启缓存。
5. 遇到疑难杂症,先清缓存再试——这招能解决80%的“幻觉问题”。

好了,构建缓存的配置就说到这里。你想想看,这些配置加起来也就几十行代码,但带来的编译提速效果却非常显著。下一步,你可以去自己项目里试试,看看增量编译从60秒降到10秒是什么体验。