2、Gradle基础入门:Groovy语法速览、Gradle Wrapper原理、settings.gradle和build.gradle文件结构、Project和Task模型

好,咱们正式开始啃 Gradle 这块硬骨头。说实话,我刚接触 Android 构建时,也被 Gradle 那一堆 .gradle 文件搞得晕头转向。但后来我发现,只要抓住几个核心概念,这东西其实挺简单的。今天我们就从最基础的东西说起。

2.1 Groovy 语法速览:你只需要知道这些

Gradle 的构建脚本是用 Groovy 写的。你不需要成为 Groovy 专家,但看懂和写简单的脚本是必须的。我挑几个最常用的点说说。

2.1.1 闭包(Closure)—— Gradle 的灵魂

闭包说白了就是一段可执行的代码块。在 Gradle 里你天天见它:

// 这是一个闭包
android {
    compileSdk 34
}

这里的 { } 就是一个闭包,它被传递给 android 方法。闭包可以访问外部变量,也可以接收参数。我个人习惯把闭包理解为「可以到处传递的函数」。

2.1.2 省略括号—— Groovy 的语法糖

Groovy 允许你省略方法调用的括号。所以下面两种写法完全等价:

// 带括号
println("Hello Gradle")
// 省略括号
println "Hello Gradle"

在 build.gradle 里,你看到的 dependencies { } 其实就是 dependencies({ }) 的简写。嗯,这里要注意:省略括号虽然爽,但嵌套多了容易看花眼。

2.1.3 字符串与 GString

单引号和双引号有区别。双引号支持插值:

def name = "Gradle"
println "Hello, $name"  // 输出:Hello, Gradle
println 'Hello, $name'  // 输出:Hello, $name(原样输出)

我在项目中遇到过有人用单引号写路径,结果变量没展开,排查了半天。所以建议:需要变量就用双引号,纯字符串用单引号。

2.1.4 集合与映射

List 和 Map 的写法也很简洁:

// List
def list = [1, 2, 3]
println list[0]  // 1

// Map
def map = [key1: 'value1', key2: 'value2']
println map.key1  // value1

你想想看,build.gradle 里的 defaultConfig { } 本质上就是在操作一个 Map。

小技巧: 如果你只是想快速验证一段 Groovy 代码,可以在项目根目录下创建一个 test.groovy 文件,然后运行 groovy test.groovy。前提是你装了 Groovy SDK。

2.2 Gradle Wrapper 原理:为什么你的项目不需要装 Gradle?

你有没有好奇过:为什么从 GitHub 拉下一个 Android 项目,不用装 Gradle 就能直接编译?答案就是 Gradle Wrapper。

Wrapper 本质上是一个脚本(gradlew + gradlew.bat)和一个配置文件(gradle-wrapper.properties)。它的工作流程很简单:

  1. 检查本地有没有指定版本的 Gradle
  2. 如果没有,自动从配置的地址下载
  3. 用下载好的 Gradle 执行构建

我曾经遇到过团队里有人直接装了个全局 Gradle 8.0,但项目要求 7.4,结果构建失败。后来强制大家用 Wrapper,再也没出过这种问题。

核心配置文件长这样:

# gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

这里最关键的就是 distributionUrl,它决定了用哪个 Gradle 版本。我建议你每次升级 Gradle 时,只改这个 URL,然后重新生成 Wrapper 文件。

注意: 不要把 gradle-wrapper.properties 加入 .gitignore。这个文件必须提交到版本控制,否则别人拉下来不知道用哪个版本。

2.3 settings.gradle 和 build.gradle 文件结构

一个 Android 项目通常有多个 .gradle 文件。新手最容易搞混的就是它们各自的作用。我画了张图帮你理清关系:

Gradle 构建文件结构 项目根目录 settings.gradle(项目配置入口) 根 build.gradle(全局配置) app/build.gradle(模块配置) library/build.gradle(模块配置) settings.gradle 决定包含哪些模块,build.gradle 决定怎么构建

2.3.1 settings.gradle:告诉 Gradle 有哪些模块

这个文件在项目根目录。它的核心作用就是声明项目包含哪些子模块:

// settings.gradle
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "MyApp"
include ':app'
include ':library'

注意 include ':app' 这一行。它告诉 Gradle:「嘿,我要把 app 这个文件夹当作一个模块来构建」。如果你新增了一个模块,记得在这里加上。

2.3.2 根 build.gradle:全局配置

根目录的 build.gradle 通常只配置两样东西:插件声明和全局属性。

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

apply false 的意思是「先声明这个插件,但别应用」。真正的应用是在子模块的 build.gradle 里。这样做的好处是:所有模块的插件版本统一管理。

2.3.3 模块 build.gradle:具体怎么构建

这才是你真正经常改的文件。它配置了编译 SDK、构建类型、依赖等:

// app/build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.myapp'
    compileSdk 34

    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
}

我在项目中遇到过有人把 compileSdktargetSdk 搞混。简单说:compileSdk 决定你能用哪些 API,targetSdk 决定你的 App 在哪个系统版本上表现最优。

核心要点: settings.gradle 管「有哪些模块」,根 build.gradle 管「用什么插件版本」,模块 build.gradle 管「具体怎么构建」。各司其职,别混在一起写。

2.4 Project 和 Task 模型:Gradle 构建的基本单元

Gradle 的构建过程,说白了就是「在 Project 上执行一系列 Task」。每个 build.gradle 文件对应一个 Project,而 Project 里包含多个 Task。

2.4.1 Project:构建的容器

每个模块都是一个 Project。根目录是一个 Project,app 模块也是一个 Project。Project 提供了:

  • 属性:比如 project.nameproject.buildDir
  • 方法:比如 project.dependencies { }project.task('hello') { }
  • 生命周期:初始化、配置、执行三个阶段

你写的 android { }dependencies { } 其实都是在调用 Project 的方法。

2.4.2 Task:构建的最小执行单元

Task 就是一个个具体的构建动作。比如编译 Java 代码是一个 Task,生成 APK 是一个 Task,运行单元测试也是一个 Task。

你可以自定义 Task:

// 在 build.gradle 中
task hello {
    doLast {
        println "Hello, Gradle!"
    }
}

然后在终端运行:

./gradlew hello

就会看到输出。你想想看,整个构建过程就是由成百上千个这样的 Task 组成的。

2.4.3 Task 依赖与执行顺序

Task 之间可以有依赖关系。比如 assembleDebug 依赖 compileDebugKotlin,所以会先编译再打包。

task taskA {
    doLast { println "执行 A" }
}

task taskB(dependsOn: taskA) {
    doLast { println "执行 B" }
}

运行 ./gradlew taskB 会先执行 A,再执行 B。我曾经在优化构建速度时,就是通过分析 Task 依赖图,找到了不必要的串行依赖,改成了并行执行,构建时间直接缩短了 30%。

调试技巧: 运行 ./gradlew tasks --all 可以列出所有可用的 Task。运行 ./gradlew :app:dependencies 可以查看依赖树。这两个命令我几乎天天用。

2.5 本章小结

这一章我们覆盖了 Gradle 构建脚本的基础。Groovy 语法你不需要背,写多了自然就熟了。Wrapper 是团队协作的基石,一定要用。三个 .gradle 文件各司其职,别搞混。Project 和 Task 模型是理解 Gradle 的关键,记住「构建就是执行 Task」就够了。

下一章我们会深入 Gradle 的生命周期和配置阶段,到时候你会更清楚「为什么 build.gradle 里的代码有时候执行了,有时候没执行」。嗯,先消化这些吧。


公众号:蓝海资料掘金营,微信 deep3321