Android 构建系统基础:Gradle、构建脚本、任务、依赖

先问个问题:你觉得 Android 项目里,真正管着编译、打包、签名那套流程的是谁?很多人第一反应是 Android Studio。其实不是。幕后那个大管家是 Gradle,一个自动化构建工具。AGP(Android Gradle Plugin)则是 Google 为 Gradle 写的“适配器”,让它能听懂 Android 特有的东西,比如资源文件、Manifest、DEX 编译。

我从 2016 年开始用 Gradle,当时刚接手一个老项目,build.gradle 有上千行,光 dependencies 就写了三个小时。后来我把它拆成模块化,用 ext 统一版本号,才算是摆脱了“改一次版本号就想吐”的噩梦。今天咱们就把这些基础核心概念拆开聊聊。

Android 构建系统核心概念 构建系统(Gradle + AGP) 构建脚本 任务(Task) 依赖管理 生命周期 Groovy / Kotlin DSL apply 与 plugins assembleDebug 依赖执行顺序 implementation / api 版本冲突解决 配置 → 执行 Gradle 本身是一个通用构建工具,AGP 是插件, 两者共同决定了 Android 项目的构建行为。

1. 构建脚本:项目的“说明书”

每个 Android 项目至少有两个 build.gradle 文件:一个在项目根目录(Project-level),一个在 app 模块下(Module-level)。前者配置全局插件和仓库,后者配置该模块的编译参数、依赖、buildTypes 等。

关键区别: Project-level 只做全局顶层设定,Module-level 才是真正干活的地方。

我习惯在根目录的 build.gradle 里统一声明 AGP 版本和 Kotlin 版本,然后用 ext 或 libs.versions.toml 来管理依赖版本号。这么做的好处是:一个地方改版本,全局同步生效。我曾经踩过一个坑——在多个模块里各写各的 dependencies 版本号,最后升级 AGP 时有些依赖没升,导致编译冲突,排查了一整天。从那以后我再也不敢分散管理版本了。


// 项目根目录 build.gradle
plugins {
    id 'com.android.application' version '8.2.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
}

// 模块级 build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}
小提示:新版 Gradle 推荐使用 plugins 块替代旧的 apply plugin,能更好地管理依赖版本声明。Gradle 7.0+ 默认就吃这套。

2. 任务(Task):编译旅程的每一个步骤

Gradle 把整个构建过程拆成一个个 Task。比如 assembleDebug、compileDebugKotlin、mergeDebugResources。每个 Task 有输入(Input)、输出(Output),以及依赖的 Task 列表。你想想看,要是一个 Task 的输入没变,Gradle 就会跳过它,直接用缓存。这就是增量构建——能大幅缩短编译时间。

我在做性能优化时,最爱用的命令就是 ./gradlew assembleDebug --scan。它会生成一个网页版构建扫描报告,清楚展示每个 Task 的耗时。有一次我发现 lint 任务竟然占了 40% 时间,赶紧加了个 lintOptions { abortOnError false },开发阶段直接省了半分钟。

Task 名称 作用 常见耗时原因
compileDebugKotlin 编译 Kotlin 源码到 JVM 字节码 增量编译失效、注解处理器过多
mergeDebugResources 合并所有模块的资源文件 大量 AAR 依赖、资源数量爆炸
transformClassesWithDexBuilder 将编译后的类文件转换为 DEX 方法数过多、MultiDex 配置不当
注意:有些 Task 是 AGP 自动生成的,比如 preBuild、check。你不要手动去 remove 它们,除非你完全清楚后果。我曾经试过在构建脚本里删掉 lint 任务,结果发现 assembleDebug 没跑完就报错了——因为别的 Task 还依赖它。

3. 依赖管理:别把整个图书馆搬过来

依赖有三层含义:一是模块间的依赖(比如 app 依赖 library),二是远程仓库的依赖(maven/jcenter),三是 Task 之间的依赖。这里我们重点说远程依赖的配置。

Gradle 3.4 之后,compile 被拆成了 implementation 和 api。简单说:implementation 的依赖只在当前模块内部可见,api 的依赖会泄漏给引用该模块的外部。我个人的建议:能用 implementation 就不要用 api。为什么?因为 api 会把依赖传递出去,增加编译时间,还容易产生版本冲突。说白了,api 是个容易引火烧身的东西。


dependencies {
    // 只有 app 内部能用,外部模块看不到
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
    
    // 外部模块也能看到这个依赖
    api 'com.google.android.material:material:1.11.0'
    
    // 只在编译时参与,不打包进 APK
    compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
    
    // 只在单元测试中使用
    testImplementation 'junit:junit:4.13.2'
}
版本冲突实战:当两个依赖引入同一个库的不同版本时,Gradle 默认使用最高版本。你可以在 build.gradle 中用 configurations.all { resolutionStrategy { force 'xxx:1.2.0' } } 强制指定。但如果乱 force,有可能会引入 API 不兼容的 bug。我曾经因为强制了一个旧版 Retrofit,结果同事写的回调接口编译过了但运行时抛 NoSuchMethodError,查了三个小时。

4. 生命周期:配置 vs 执行

Gradle 构建分两个阶段:配置阶段和执行阶段。配置阶段读取所有 build.gradle 并创建 Task 的 DAG(有向无环图);执行阶段再按顺序跑 Task。

你可能会问:为什么 Gradle 启动后要等好几秒才跑编译?那其实就是在搞配置阶段,解析脚本、注册 Task。如果你的 build.gradle 里写了太多 I/O 操作或网络请求(比如每个 build 都去拉一个版本号),配置阶段会慢得让人抓狂。我遇到过同事在配置阶段调一个 HTTP 接口来获取构建时间,结果每次构建多花 8 秒。嗯,后来被我挪到 Task 里延迟执行了。

优化思路:尽量把耗时操作写在 Task 的 doLast 或 doFirst 里,而不是直接写在脚本顶层。脚本顶层属于配置阶段,会影响所有 Gradle 命令,甚至 ./gradlew tasks 这种“单纯列任务列表”的命令都会被拖慢。

5. 再聊几句实践

好了,核心概念就这么些。你要是能搞明白构建脚本、Task、依赖、生命周期这四样东西,看 AGP 升级带来的构建网络瀑布图变化就轻松多了——因为瀑布图本质就是记录 Task 执行时的网络请求耗时与依赖关系。

我最后想强调一个容易忽视的点:Gradle Daemon。它是一个后台进程,专门用来缓存项目状态和编译信息。如果你执行 ./gradlew --stop,下次构建会重新启动 Daemon。如果你发现每次第一次构建特别慢,后面几次快很多,那是 Daemon 救了你。别老去手动杀它,除非你改了 Gradle 版本或 AGP 插件。

好啦,基础内容差不多就是这些。下一块我们会把目光聚焦到真正的瀑布图变化上,看看 AGP 版本升级后,哪些 Task 的网络 I/O 变了,哪些依赖下载路径变了——那才是我们课程的核心。