Skip to content

常见深坑避雷 ​

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.0
kotlin
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=false

Gradle 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 注解
  • 支持增量构建
  • 支持缓存

避免绝对路径:

  • 使用相对路径
  • 使用 layout API
  • 支持跨平台构建

监控性能:

  • 定期生成 Profile
  • 使用 Build Scan
  • 优化慢速任务