常见深坑避雷
Gradle 最让人困惑的是它的执行逻辑。很多开发者习惯了顺序执行的代码,但在 Gradle中,如果不理解生命周期,代码行为会完全超出预期。
配置阶段vs执行阶段
配置阶段耗时操作 致命错误
❌ 错误示例:
kotlin
// build.gradle.kts
tasks.register("fetchData") {
// 这里在配置阶段执行!无论运行什么任务都会执行
val data = URL("https://api.example.com/data").readText()
doLast {
println("Data: $data")
}
}后果:
- 每次 IDE 同步项目都会发起网络请求
- 运行任何任务都会执行该代码
- 导致 IDE 卡顿
- 构建变慢
✅ 正确做法:
kotlin
tasks.register("fetchData") {
doLast {
// 仅在执行该任务时运行
val data = URL("https://api.example.com/data").readText()
println("Data: $data")
}
}配置阶段常见错误
❌ 读取文件:
kotlin
// 配置阶段执行,每次都读取
val content = file("data.txt").readText()✅ 延迟读取:
kotlin
val content = providers.fileContents(
layout.projectDirectory.file("data.txt")
).asText❌ 执行命令:
kotlin
// 配置阶段执行
val gitHash = "git rev-parse HEAD".execute().text✅ 使用 Provider:
kotlin
val gitHash = providers.exec {
commandLine("git", "rev-parse", "HEAD")
}.standardOutput.asText任务创建:create vs register
性能陷阱
❌ 使用 create(立即创建):
kotlin
// 立即创建并配置,即使不使用也会消耗资源
tasks.create("myTask") {
// 配置代码
}✅ 使用 register(延迟创建):
kotlin
// 仅在需要时创建和配置
tasks.register("myTask") {
// 配置代码仅在任务被使用时执行
}性能对比:
create:所有任务立即创建,配置阶段慢register:按需创建,配置阶段快 30%+
访问注册的任务
❌ 错误方式:
kotlin
tasks.register("myTask")
tasks.getByName("myTask") // 强制实例化✅ 正确方式:
kotlin
val myTask = tasks.register("myTask")
// 需要时才访问
myTask.configure {
// 配置
}依赖配置陷阱
动态版本号 生产禁用
❌ 使用动态版本:
kotlin
dependencies {
implementation("com.example:library:+")
implementation("com.example:library:1.0.+")
implementation("com.example:library:latest.release")
}后果:
- 每次构建结果可能不同
- 无法离线构建
- 缓存频繁失效
- 依赖解析变慢
- 生产风险高
✅ 使用固定版本:
kotlin
dependencies {
implementation("com.example:library:1.0.5")
}传递依赖冲突
❌ 忽略冲突:
Dependency conflict:
app -> lib-a:1.0 -> common:1.0
app -> lib-b:2.0 -> common:2.0✅ 显式解决:
kotlin
dependencies {
implementation("com.example:lib-a:1.0") {
exclude(group = "com.example", module = "common")
}
implementation("com.example:lib-b:2.0")
implementation("com.example:common:2.0")
}配置VS实现依赖
❌ 混淆 api 和 implementation:
kotlin
// 库模块不应该使用 api 暴露所有依赖
dependencies {
api("com.squareup.retrofit2:retrofit:2.9.0")
api("com.squareup.okhttp3:okhttp:4.12.0")
api("com.google.code.gson:gson:2.10")
}后果:
- 依赖传递到使用者
- 编译时间增加
- 依赖冲突风险
✅ 仅暴露必要依赖:
kotlin
dependencies {
api("com.example:public-api:1.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.google.code.gson:gson:2.10")
}路径和文件陷阱
绝对路径硬编码 破坏缓存
❌ 使用绝对路径:
kotlin
val dataFile = File("/Users/admin/project/data.txt")
val outputDir = File("C:\\Users\\Admin\\output")后果:
- 其他开发者无法构建
- 缓存无法命中
- CI/CD 失败
✅ 使用相对路径:
kotlin
val dataFile = layout.projectDirectory.file("data.txt")
val outputDir = layout.buildDirectory.dir("output")文件路径分隔符
❌ 硬编码分隔符:
kotlin
val path = project.projectDir.toString() + "/src/main/java"✅ 使用 File API:
kotlin
val path = file("src/main/java")
val sourceDir = project.projectDir.resolve("src/main/java")afterEvaluate 滥用
为什么避免 afterEvaluate
❌ 过度使用:
kotlin
afterEvaluate {
dependencies {
implementation("com.example:lib:1.0")
}
}
afterEvaluate {
android {
compileSdkVersion(34)
}
}问题:
- 执行顺序不确定
- 多模块项目中出现竞态条件
- 难以调试
- 性能下降
✅ 使用 Lazy API:
kotlin
val compileSdk = providers.gradleProperty("compileSdk")
.map { it.toInt() }
.orElse(34)
android {
compileSdkVersion(compileSdk.get())
}唯一合理的使用场景
kotlin
// 仅在必须等待Android插件完全配置后才能访问的情况
afterEvaluate {
android.applicationVariants.all { variant ->
// 只能在这里访问 variant
}
}更好的方式(AGP 7.0+):
kotlin
androidComponents {
onVariants { variant ->
// 新的 Variant API
}
}多模块问题
循环依赖
❌ 循环依赖:
:app -> :core -> :utils -> :core # 循环后果:
- 构建失败
- 无法增量构建
✅ 重构依赖关系:
:app -> :core -> :utils
-> :common模块间属性传递
❌ 直接访问子模块:
kotlin
// 根 build.gradle.kts
val appVersion = project(":app").version问题:
- 强制配置子项目
- 破坏配置缓存
✅ 使用共享属性:
properties
# gradle.properties
app.version=1.0.0kotlin
val appVersion: String by project缓存问题
inputs/outputs 未声明
❌ 未声明输入输出:
kotlin
abstract class MyTask : DefaultTask() {
@TaskAction
fun execute() {
val input = File("input.txt").readText()
File("output.txt").writeText(input.uppercase())
}
}后果:
- 任务总是执行
- 缓存无效
✅ 声明输入输出:
kotlin
abstract class MyTask : DefaultTask() {
@InputFile
abstract val inputFile: RegularFileProperty
@OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun execute() {
val input = inputFile.get().asFile.readText()
outputFile.get().asFile.writeText(input.uppercase())
}
}非确定性任务
❌ 使用时间戳:
kotlin
tasks.register("generateFile") {
doLast {
val timestamp = System.currentTimeMillis()
file("output.txt").writeText("Generated at $timestamp")
}
}后果:
- 每次输出不同
- 缓存永远不命中
✅ 避免非确定性输入:
kotlin
tasks.register("generateFile") {
val version = project.version.toString()
doLast {
file("output.txt").writeText("Version: $version")
}
}Android 特定陷阱
BuildConfig 生成问题 AGP 8.0+
❌ 假设 BuildConfig 存在:
kotlin
// AGP 8.0+ 默认不生成 BuildConfig
if (BuildConfig.DEBUG) {
// 编译错误!
}✅ 显式启用:
kotlin
android {
buildFeatures {
buildConfig = true
}
}资源ID非常量 AGP 8.0+
❌ switch 中使用资源 ID:
java
// AGP 8.0+ 资源ID不再是常量
switch (viewId) {
case R.id.button: // 编译错误!
break;
}✅ 使用 if-else:
kotlin
when (viewId) {
R.id.button -> {
// Kotlin when 可以使用
}
}Kapt处理器问题
❌ 未启用增量编译:
properties
# 未配置 kapt后果:
- Kapt 非常慢
- 全量重新生成
✅ 启用优化:
properties
# gradle.properties
kapt.incremental.apt=true
kapt.use.worker.api=true
kapt.include.compile.classpath=falseGradle Wrapper 问题
版本不统一
❌ 团队成员使用不同 Gradle 版本:
后果:
- 构建结果不一致
- 插件兼容性问题
✅ 使用 Gradle Wrapper:
bash
# 更新 wrapper
./gradlew wrapper --gradle-version 8.10提交 gradle/wrapper/ 和 gradlew 到版本控制。
Wrapper 验证
✅ 启用校验和验证:
bash
# gradle/wrapper/gradle-wrapper.properties
distributionSha256Sum=abc123...性能陷阱
配置阶段慢
常见原因:
- 配置阶段执行耗时操作
- 使用
create而非register - 过度使用
afterEvaluate
解决方案:
- 使用
--profile分析 - 延迟执行耗时操作
- 使用 Lazy API
任务执行慢
常见原因:
- 未启用并行构建
- 未启用构建缓存
- 任务输入输出未优化
解决方案:
properties
# gradle.properties
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.vfs.watch=true最佳实践
理解生命周期:
- Initialization:确定项目结构
- Configuration:配置任务
- Execution:执行任务
使用延迟 API:
tasks.register代替create- Provider API
- Lazy properties
避免配置阶段操作:
- 不执行网络调用
- 不读取大文件
- 不执行系统命令
使用固定版本:
- 避免动态版本号
- 使用 Version Catalog
- 锁定依赖版本
声明任务输入输出:
- 使用
@Input、@Output注解 - 支持增量构建
- 支持缓存
避免绝对路径:
- 使用相对路径
- 使用
layoutAPI - 支持跨平台构建
监控性能:
- 定期生成 Profile
- 使用 Build Scan
- 优化慢速任务