Gradle 构建概览

Android 应用通常使用 Gradle 构建系统构建。在深入了解如何配置 build 之前,我们将先探讨 build 背后的概念,以便您从整体上了解该系统。

什么是 build?

构建系统将源代码转换为可执行的应用。构建通常涉及多种工具,用于分析、编译、链接和打包应用或库。Gradle 使用基于任务的方法来整理和运行这些命令。

任务封装了将输入转换为输出的命令。插件定义任务及其配置。将插件应用于 build 会注册其任务,并使用这些任务的输入和输出将它们连接在一起。例如,将 Android Gradle 插件 (AGP) 应用于 build 文件将注册构建 APK 或 Android 库所需的所有任务。借助 java-library 插件,您可以从 Java 源代码构建 JAR。Kotlin 和其他语言也有类似的插件,但其他插件旨在扩展插件。例如,protobuf 插件旨在为 AGP 或 java-library 等现有插件添加 protobuf 支持。

Gradle 倾向于使用惯例而非配置,因此插件会提供良好的默认值,但您可以通过声明性领域特定语言 (DSL) 进一步配置 build。DSL 的设计旨在让您指定要构建的内容,而不是构建方式。插件中的逻辑负责管理“如何”。该配置在项目(和子项目)的多个build 文件中指定。

任务输入可以是文件和目录,也可以是编码为 Java 类型(整数、字符串或自定义类)的其他信息。输出只能是目录或文件,因为它们必须写入磁盘。将一个任务的输出连接到另一个任务的输入,可将这两个任务关联起来,以便一个任务必须在另一个任务之前运行。

虽然 Gradle 支持在 build 文件中编写任意代码和任务声明,但这可能会使工具更难理解您的 build,也更难维护。例如,您可以为插件中的代码编写测试,但不能为 build 文件中的代码编写测试。相反,您应将 build 逻辑和任务声明限制在插件(由您或其他人员定义)中,并在 build 文件中声明您希望如何使用该逻辑。

Gradle build 运行时会发生什么?

Gradle build 分三个阶段运行。每个阶段都会执行您在 build 文件中定义的不同部分的代码。

  • 初始化用于确定哪些项目和子项目包含在 build 中,并设置包含 build 文件和已应用插件的类路径。此阶段的重点是设置文件,您可以在其中声明要构建的项目以及从中获取插件和库的位置。
  • 配置会为每个项目注册任务,并执行 build 文件以应用用户的 build 规范。请务必注意,您的配置代码将无法访问执行期间生成的数据或文件。
  • 执行会实际“构建”您的应用。配置的输出是一个任务有向无环图 (DAG),表示用户请求的所有必需构建步骤(在命令行上或在 build 文件中作为默认值提供的任务)。此图表示任务之间的关系,可以是任务声明中明确指出的关系,也可以是基于任务的输入和输出的关系。如果某个任务的输入是另一任务的输出,则该任务必须在另一任务之后运行。此阶段会按图中定义的顺序运行过时的任务;如果任务的输入自上次执行以来未发生变化,Gradle 将跳过该任务。

如需了解详情,请参阅 Gradle build 生命周期

配置 DSL

Gradle 使用领域专用语言 (DSL) 来配置 build。这种声明式方法侧重于指定数据,而不是编写分步(命令式)说明。您可以使用 Kotlin 或 Groovy 编写 build 文件,但我们强烈建议使用 Kotlin。

DSL 旨在让领域专家和程序员等所有人都能更轻松地为项目做出贡献,方法是定义一种以更自然的方式表示数据的小型语言。Gradle 插件可以扩展 DSL,以配置其任务所需的数据。

例如,配置 build 的 Android 部分可能如下所示:

Kotlin

android {
    namespace = "com.example.app"
    compileSdk {
        version = release(36) {
            minorApiLevel = 1
        }
    }
    // ...

    defaultConfig {
        applicationId = "com.example.app"
        minSdk {
            version = release(23)
        }
        targetSdk {
            version = release(36)
        }
        // ...
    }
}

Groovy

android {
    namespace = 'com.example.app'
    compileSdk {
        version = release(36) {
            minorApiLevel = 1
        }
    }
    // ...

    defaultConfig {
        applicationId = 'com.example.app'
        minSdk {
            version = release(23)
        }
        targetSdk {
            version = release(36)
        }
        // ...
    }
}

在后台,DSL 代码类似于:

fun Project.android(configure: ApplicationExtension.() -> Unit) {
    ...
}

interface ApplicationExtension {
    var namespace: String?

    fun compileSdk(configure: CompileSdkSpec.() -> Unit) {
        ...
    }

    val defaultConfig: DefaultConfig

    fun defaultConfig(configure: DefaultConfig.() -> Unit) {
        ...
    }
}

DSL 中的每个块都由一个函数表示,该函数接受一个用于配置它的 lambda,以及一个用于访问它的同名属性。这使得 build 文件中的代码更像数据规范。

外部依赖项

Maven 构建系统引入了依赖项规范、存储和管理系统。库存储在代码库(服务器或目录)中,其元数据包括版本和对其他库的依赖关系。您可以指定要搜索的代码库、要使用的依赖项版本,然后构建系统会在构建期间下载它们。

Maven 制品通过组名称(公司、开发者等)、制品名称(库的名称)和相应制品的版本进行标识。这通常表示为 group:artifact:version

这种方法可显著改进 build 管理。您经常会听到此类代码库被称为“Maven 代码库”,但实际上,这完全取决于制品打包和发布的方式。这些代码库和元数据已在多个 build 系统(包括 Gradle)中重复使用(Gradle 也可以发布到这些代码库)。公共仓库允许所有人共享使用,而公司仓库则可将内部依赖项保留在公司内部。

您还可以将项目模块化子项目(在 Android Studio 中也称为“模块”),这些子项目也可以用作依赖项。每个子项目都会生成可供子项目或顶级项目使用的输出(例如 jar)。这样可以隔离需要重建的部分,从而缩短 build 时间,并更好地分离应用中的职责。

我们将在添加 build 依赖项中详细介绍如何指定依赖项。

build 变体

创建 Android 应用时,您通常需要构建多个变体。变体包含不同的代码或使用不同的选项构建,并且由 build 类型和产品变种组成。

build 类型会改变声明的 build 选项。默认情况下,AGP 会设置“release”和“debug”build 类型,但您可以调整这些类型并添加更多类型(可能用于预发布或内部测试)。

调试 build 不会缩小或混淆应用,从而加快其 build 速度并按原样保留所有符号。它还会将应用标记为“可调试”,使用通用调试密钥对其进行签名,并允许访问设备上已安装的应用文件。这样一来,您就可以在运行应用的同时探索文件和数据库中的已保存数据。

发布 build 会优化应用,使用您的发布密钥为应用签名,并保护已安装的应用文件。

借助产品变种,您可以更改应用中包含的源代码和依赖项变种。例如,您可能需要为应用创建“demo”和“full”变种,或者“free”和“paid”变种。您可以在“main”源代码集目录中编写通用源代码,并在以相应特性命名的源代码集中替换或添加源代码。

AGP 会为 build 类型和产品变种的每种组合创建变体。如果您未定义变种,则变体将以 build 类型命名。如果您同时定义了这两个参数,则变体的名称为 <flavor><Buildtype>。例如,如果 build 类型为 releasedebug,变种为 demofull,AGP 将创建以下变体:

  • demoRelease
  • demoDebug
  • fullRelease
  • fullDebug

后续步骤

现在,您已经了解了 build 概念,接下来请查看项目中的 Android build 结构