Android Gradle 笔记

13 阅读4分钟

一、Gradle 基础认知

1.1 Gradle 是什么?

Gradle 是一个基于 Groovy 或 Kotlin DSL 的自动化构建工具,它结合了 Ant 的灵活性和 Maven 的依赖管理能力,并且引入了“约定优于配置”的理念。在 Android 项目中,Google 提供了 Android Gradle Plugin (AGP),它与 Gradle 协同工作,专门用于构建 Android 应用。

1.2 核心概念

  • Project:一个 Gradle 构建对应一个或多个 Project。在 Android 中,根目录是一个 Project,每个模块(如 app、library)也是一个 Project。
  • Task:构建的最小工作单元,例如编译 Java 代码、打包资源、生成 APK 等。Gradle 通过 Task 依赖图来执行构建。
  • Plugin:插件封装了通用的构建逻辑,如 com.android.application 插件提供了构建 App 所需的所有 Task 和配置。
  • Build Lifecycle:Gradle 构建分为三个阶段:
    1. 初始化:解析 settings.gradle,确定哪些 Project 参与构建。
    2. 配置:执行每个 Project 的 build.gradle,生成 Task 依赖图。
    3. 执行:根据命令行参数执行指定的 Task。

1.3 Android 项目结构

一个典型的 Android 项目包含以下 Gradle 文件:

  • settings.gradle:声明项目包含的模块。
  • build.gradle (Project level):全局配置,如仓库地址、插件依赖、全局变量。
  • build.gradle (Module level):每个模块的具体配置,如 android 闭包、依赖项。
  • gradle.properties:JVM 参数、Gradle 属性(如并行编译开关)。
  • gradle-wrapper.properties:指定 Gradle 版本和下载地址。

二、Android Gradle Plugin 核心配置

2.1 基本配置

在模块的 build.gradle 中,android 闭包是我们最常打交道的区域:

android {
    compileSdk 34
    buildToolsVersion "34.0.0"

    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 21
        targetSdk 34
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    // 签名配置
    signingConfigs {
        release {
            storeFile file("keystore.jks")
            storePassword "123456"
            keyAlias "key"
            keyPassword "123456"
        }
    }

    // 构建类型
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
        debug {
            minifyEnabled false
            debuggable true
        }
    }

    // 产品变种(多渠道)
    productFlavors {
        free {
            dimension "version"
            applicationIdSuffix ".free"
            versionNameSuffix "-free"
        }
        pro {
            dimension "version"
            applicationIdSuffix ".pro"
            versionNameSuffix "-pro"
        }
    }

    // 源集配置(可自定义代码和资源目录)
    sourceSets {
        main {
            java.srcDirs = ['src/main/java']
            res.srcDirs = ['src/main/res']
        }
        free {
            java.srcDirs = ['src/free/java']
        }
    }

    // 编译选项
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    // Kotlin 选项(如果使用 Kotlin)
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

2.2 Build Variants(构建变种)

构建变种 = Build Type(如 debug/release) × Product Flavor(如 free/pro)。每个变种都有自己的输出 APK 名称、代码和资源源集(src/freeDebug/)。使用变种可以实现同一套代码生成不同版本的应用(如免费版/付费版、不同渠道包)。

2.3 依赖管理

2.3.1 依赖配置类型

  • implementation:只在当前模块内部使用,不会泄露给其他模块。推荐首选。
  • api:将依赖暴露给其他模块,适用于公共库。
  • compileOnly:仅编译时使用,不打包进 APK(如注解处理器)。
  • runtimeOnly:仅运行时需要,编译时不需要。
  • annotationProcessor / kapt:注解处理器。
  • testImplementation:仅用于单元测试。
  • androidTestImplementation:仅用于 Android 测试。

2.3.2 依赖传递与排除

dependencies {
    implementation('com.example:library:1.0') {
        exclude group: 'com.unwanted', module: 'unwanted-module'
        transitive = false // 禁止传递依赖
    }
}

2.3.3 版本冲突解决

Gradle 默认会选择最高版本,但可能会遇到冲突。可以通过以下方式强制指定版本:

configurations.all {
    resolutionStrategy {
        force 'com.squareup.okhttp3:okhttp:4.11.0'
        // 或者动态选择策略
        failOnVersionConflict()
    }
}

三、实际使用案例

案例1:统一版本管理

随着项目模块增多,手动维护版本号容易出错。我们通常采用以下几种方式统一管理:

方式一:使用 ext 全局变量(传统)

在项目根 build.gradle 中定义:

ext {
    compileSdkVersion = 34
    minSdkVersion = 21
    targetSdkVersion = 34
    versionCode = 1
    versionName = '1.0'

    // 依赖版本
    androidxAppcompat = '1.6.1'
    retrofit = '2.9.0'
}

然后在模块中引用:

android {
    compileSdk rootProject.ext.compileSdkVersion
    // ...
}

dependencies {
    implementation "androidx.appcompat:appcompat:$rootProject.ext.androidxAppcompat"
}

方式二:Gradle Version Catalog(推荐,Gradle 7.0+)

创建 gradle/libs.versions.toml 文件:

[versions]
compileSdk = "34"
minSdk = "21"
targetSdk = "34"
appcompat = "1.6.1"

[libraries]
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }

在模块中:

dependencies {
    implementation(libs.androidx.appcompat)
}

并可结合 buildSrc 或复合构建实现更高级的复用。

案例2:多渠道打包(友盟统计)

以友盟渠道为例,通常需要为每个渠道生成不同的 APK,并在 AndroidManifest.xml 中替换渠道号。

配置 productFlavors

flavorDimensions "channel"
productFlavors {
    google {
        dimension "channel"
    }
    huawei {
        dimension "channel"
    }
    xiaomi {
        dimension "channel"
    }
}

// 批量配置,避免重复
productFlavors.all { flavor ->
    // 为每个 flavor 添加一个 manifest placeholder
    flavor.manifestPlaceholders = [UMENG_CHANNEL: name]
}

AndroidManifest.xml 中使用占位符:

<meta-data
    android:name="UMENG_CHANNEL"
    android:value="${UMENG_CHANNEL}" />

生成不同应用名或图标:可以在对应 flavor 的源集目录(如 src/google/res/values/strings.xml)中覆盖字符串资源。

案例3:自定义构建配置(如自动生成 BuildConfig 字段)

有时候我们需要在代码中获取一些构建时信息,比如 Git 提交号、编译时间。可以通过在 build.gradle 中动态添加 BuildConfig 字段实现:

android {
    defaultConfig {
        buildConfigField "String", "GIT_COMMIT", "\"${getGitCommit()}\""
        buildConfigField "long", "BUILD_TIME", "${System.currentTimeMillis()}L"
    }
}

def getGitCommit() {
    try {
        def stdout = new ByteArrayOutputStream()
        exec {
            commandLine 'git', 'rev-parse', '--short', 'HEAD'
            standardOutput = stdout
        }
        return stdout.toString().trim()
    } catch (Exception e) {
        return "unknown"
    }
}

然后在 Java/Kotlin 代码中通过 BuildConfig.GIT_COMMIT 访问。

案例4:自定义 Task 实现自动化

假设我们需要在编译前自动生成一个版本信息文件,可以编写自定义 Task:

task generateVersionFile {
    doLast {
        def versionFile = new File("$buildDir/generated/version/version.txt")
        versionFile.parentFile.mkdirs()
        versionFile.text = """
            Version Name: ${android.defaultConfig.versionName}
            Version Code: ${android.defaultConfig.versionCode}
            Build Time: ${new Date()}
        """.trim()
    }
}

// 让预编译任务依赖此 Task
preBuild.dependsOn generateVersionFile

如果想让生成的资源能被代码访问,可以将其添加到源集:

android.sourceSets.main.assets.srcDirs += "$buildDir/generated/version"

案例5:优化构建速度

大型项目中构建速度是痛点。我们可以做以下优化:

  1. 启用 Gradle 构建缓存(本地和远程):

    # gradle.properties
    org.gradle.caching=true
    
  2. 启用并行编译和配置缓存

    org.gradle.parallel=true
    org.gradle.configuration-cache=true
    
  3. 使用最新版 Gradle 和 AGP:每个新版本都包含性能改进。

  4. 按需配置:使用 includeBuild 替代 project 依赖,或在开发时只加载需要的模块。

  5. 避免动态依赖版本:如 + 会导致每次检查新版本,应指定固定版本。

  6. 分析构建耗时:运行 ./gradlew build --scan 生成构建报告,分析瓶颈。

  7. 优化自定义 Task:确保自定义 Task 正确配置输入输出,避免不必要的重复执行。

案例6:多模块依赖管理

在大型组件化项目中,模块众多,依赖关系复杂。我们常用以下几种方式简化:

  • 定义依赖版本常量:如前所述。
  • 使用 BOM(Bill of Materials):如 Firebase BOM,统一管理一组库的版本。
  • 自定义 Gradle 插件:封装通用配置,避免在每个模块重复写相同的代码。

四、常见问题与解决方案

4.1 依赖冲突

现象Duplicate classDexArchiveMergerException
排查:运行 ./gradlew :app:dependencies 查看依赖树,找出冲突。
解决:使用 excludeforce 指定版本。

4.2 构建缓慢

  • 检查是否使用了动态版本。
  • 检查自定义 Task 是否正确地使用了 @Input@Output 注解。
  • 使用 --parallel--configure-on-demand

4.3 Manifest 合并错误

现象Manifest merger failed
解决:在 AndroidManifest 中添加 tools:replace 属性,或在模块的 build.gradle 中设置 android.defaultConfig.manifestPlaceholders

4.4 Gradle 版本兼容性

AGP 对 Gradle 版本有严格对应关系(见 官方文档)。升级时要同时匹配。

4.5 多模块间资源冲突

现象:资源 ID 重复。
解决:为每个模块设置 resourcePrefix "module_"


五、进阶:编写自定义 Gradle 插件

当项目足够大,通用的配置和 Task 越来越多,可以将其抽取成自定义插件,便于复用和版本管理。

5.1 插件实现方式

  • buildSrc:在项目根目录创建 buildSrc 模块,自动被编译为插件,但变更后需重新构建整个项目。
  • 独立插件项目:发布到 Maven 仓库供多个项目使用。

5.2 简单示例(buildSrc 方式)

  1. 创建 buildSrc/src/main/java/com/example/MyPlugin.groovy(或 Kotlin):
import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.task('myTask') {
            doLast {
                println "Hello from MyPlugin in ${project.name}"
            }
        }
    }
}
  1. buildSrc/build.gradle 中配置:
apply plugin: 'groovy'
dependencies {
    implementation gradleApi()
    implementation localGroovy()
}
  1. 在其他模块中应用插件:
apply plugin: com.example.MyPlugin

5.3 高级用法:扩展

插件通常提供 DSL 扩展,让用户配置。例如:

class MyPluginExtension {
    String message = "default"
}

class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        def extension = project.extensions.create('myConfig', MyPluginExtension)
        project.task('myTask') {
            doLast {
                println "Message: ${extension.message}"
            }
        }
    }
}

使用方:

myConfig {
    message = "Hello World"
}

六、总结

Gradle 是 Android 开发中不可或缺的一环,掌握它不仅能让我们高效地管理项目,还能通过自动化解决重复劳动。回顾十年历程,从最初被其陡峭的学习曲线折磨,到如今能够随心所欲地定制构建流程,我深刻体会到:理解 Gradle 的核心概念(Task、Plugin、生命周期)比死记硬背语法更重要。实际项目中,建议从小处着手,逐步引入自动化脚本,不断优化构建速度和依赖管理。