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
  • 优化慢速任务