Theme Preview

Hue:

You are using an outdated browser that does not support OKLCH colors. The color setting will not take effect.

Gradle之旅(4):Android环境下的Gradle

4799 字

一、简介

Android Gradle Plugin (以下简称AGP) 是为 Android 开发的一个 Gradle 插件,是构建 Android 应用的关键所在。可以说 Gradle 是 AGP 运行的基础,在构建过程中 Gradle 会根据项目中的配置(如 build.gradle 文件)加载并执行 AGP 插件,从而完成 Android 项目的构建任务。

那么随着 Gradle 的版本升级,为了适配 Gradle 的新特性, AGP 也在不断地升级迭代,这就产生了兼容性的问题,一般来说较新的 AGP 需要较新版本的 Gradle 来支持,具体对应版本可参见官网AGP版本说明

由于在之前的系列文章中已经对 Gradle 的各个方面做了比较详细的介绍,因此本篇着重于介绍 AGP(版本7.0.3) 所提供的功能配置,其他方面不再赘述。

二、目录结构

当我们新建一个 Android 工程之后,可以看到有如下目录结构:

$ tree -L 2

GradleDemo
.
├── app // ①
│   ├── build.gradle
│   ├── libs
│   ├── proguard-rules.pro
│   └── src
├── build.gradle // ②
├── gradle
│   └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties // ③
├── mylibrary // ④
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── libs
│   ├── proguard-rules.pro
│   └── src
└── settings.gradle // ⑤

上述目录结构中和之前的通过命令行创建的目录内容基本一致,这里只选择其中一部分来做说明:

① : Android 应用的主要模块目录
② : 项目的全局构建脚本
③ : 包含本地环境特有的配置,如SDK的路径
④ : 库模块目录
⑤ : 指定项目中包含的所有模块,通过include语句来指导 Gradle 正确构建不同模块以及他们之间的依赖关系

三、配置属性

AGP 可以配置很多属性,用于指导 Android 在构建过程中的行为,这里可以直接从官网配置build介绍获得相关信息,但这一部分介绍的内容相对较少,更多配置信息可以在Gradle APIcom.android.build.api.dsl 这个包下查看。下面我们分别从这个项目中的几个构建脚本入手,来对 AGP 的构建属性展开介绍;

3.1 settings.gradle

/*
pluginManagement 脚本块,用于配置Gradle插件的Maven仓库,配置的是构建过程中使用的仓库 ;
pluginManagement 脚本块中的 repositories 配置 , 对应之前的 buildscript 中的 repositories 配置 ;
*/
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

/**
用于配置依赖的Maven仓库 ,配置的是工程或模块下的依赖使用的仓库
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 代码的含义是解析依赖时 , 只能使用本脚本块中的 Maven 仓库 , 不能使用 Module 子项目中的依赖 ;

repositoriesMode 模式有两种 :
RepositoriesMode.PREFER_PROJECT : 解析依赖库时 , 优先使用本地仓库 , 本地仓库没有该依赖 , 则使用远程仓库 ;
RepositoriesMode.FAIL_ON_PROJECT_REPOS : 解析依赖库时 , 强行使用远程仓库 , 不管本地仓库有没有该依赖库 ;
dependencyResolutionManagement 脚本块中的 repositories 配置 , 对应之前的 allprojects 中的 repositories 配置 ;
*/
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}

rootProject.name = "GradleDemo"
include ':app'
include ':mylibrary'

3.2 project/build.gradle

这里的构建脚本是指在项目根目录下的 build.gradle :

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.0.3' apply false
id 'com.android.library' version '7.0.3' apply false
}

配置 Gradle 插件 , 下面分析每个配置的含义 :

  • 引入 com.android.application 和 library 插件;
  • version ‘7.0.3’ 说明引入插件的版本号;
  • apply false 表示当前不会马上引用该插件,在 Module 子项目中使用到该插件时,才能正式应用;

在此处主要是为了说明 Gradle 插件的版本没有其它含义;

3.3 app/build.gradle

plugins {
id 'com.android.application' // ①
}

android { // ②
namespace 'com.zsk.gradledemo'
compileSdk 31

defaultConfig {
applicationId "com.zsk.gradledemo"
minSdk 24
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
......

这个构建脚本中只摘出和 AGP 相关的配置项:

① : 在 Module 子项目中应用,没有带 apply false 表示当前模块应用此插件,并且将此模块构建程APP应用
② : AGP 扩展属性

在 AGP 3.4.0 之前,我们可以通过 googlesource 网站下载对应版本的 AGP 源码如:

//同下载AOSP过程一样,同样也可以使用国内的镜像
$ repo init -u https://android.googlesource.com/platform/manifest -b gradle_3.4.0

之后的版本可以通过如下方式获取,至于 AGP 相关属性的了解,可以通过官网开放的文章 配置buildGradle API 的方式。在 build.gradle 脚本中通过如下方式获取 AGP 源码:

implementation "com.android.tools.build:gradle:7.0.3"

这样就可以在 gradle 的缓存目录下找到如下几个目录:

$ tree -L 2
.
├── 45b02397c6fcb8241d6bfc8d75292cb3e285084
│   └── gradle-7.0.3-javadoc.jar
├── 90cbb46c2ce484cb4f6cf4fd3e7898938a7bdfa8
│   └── gradle-7.0.3.jar
├── d26689d255d877e5643c6a473b9400a1d99966e
│   └── gradle-7.0.3-sources.jar
......

其中 gradle-7.0.3-sources.jar 就是我们需要的 AGP 源码,解压之后即可查看,或者可以通过类似 Android studio 中查看也更为方便。

根据之前的系列文章可以看出来,在 build.gradle 配置脚本中, android 是 AGP 提供的扩展属性,其中 defaultConfig 是嵌套扩展属性,buildTypes 是变体,这些内容可以在源码中自行查看也可参考 旧版本3.3 中的文档但有所变动。

3.4 module/build.gradle

plugins {
id 'com.android.library'
}

android {
namespace 'com.zsk.mylibrary'
compileSdk 31

defaultConfig {
minSdk 24

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
......

}
......

moudle 模块下的构建文件和 app 模块下的有一些不同,表现在如下两点,除此之前相关配置内容都大同小异:

  1. 使用 library 插件,表明当前 module 需要编译为 aar 或 jar 等库文件
  2. 删去 applicationId 标识

四、常用示例

4.1 buildTypes

这种构建类型的设计主要是为了区分开发周期中的不同阶段,比如 debug/test/pre/release 等。这些构建类型在功能上对用户来说可能没有太大区别,‌但它们在实际应用中扮演着不同的角色。‌例如,‌调试版本可能会打印日志或执行调试代码,‌而发布版本则更加注重性能和安全性,‌可能不包含调试信息或进行了代码混淆。‌buildTypes 还涉及到应用的签名配置、‌混淆文件的设置等,‌确保应用在不同构建类型下具有适当的配置和优化。

现在我们来实现这样一个功能,要求同时输出 dev/debug/release/ 版本,且版本号带有相应标识以及具有不同的应用 ID、应用名称、应用图标:

import java.text.SimpleDateFormat

plugins {
id 'com.android.application'
}

def getGitHash() {
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
// 处理异常,例如返回固定的版本号
def dformat = "yyyy-MM-dd-HH-mm"
def timeMatter = new SimpleDateFormat(dformat)
String timeStr = timeMatter.format(new Date())
String version = timeStr + "-" + stdout.toString().trim()
return version
} catch (Exception e) {
// 处理异常,例如返回固定的版本号
def dateFormat = "yyyy-MM-dd-HH-mm"
def formatter = new SimpleDateFormat(dateFormat)
return formatter.format(new Date())
}
}

android {
namespace 'com.zsk.gradledemo'
compileSdk 31

defaultConfig {
applicationId "com.zsk.gradledemo"
minSdk 24
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}


signingConfigs {
release {
keyAlias "key0"
keyPassword "123456789"
storeFile file("sign.jks")
storePassword "123456789"
}
}

// 配置变体的命名规则
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "${project.name}-${variant.versionName}.apk"
}
}

buildTypes {
def apVersionName = getGitHash() //设置版本号为年月日 + 代码 commit 版本
debug {
versionNameSuffix "-" + apVersionName + "-debug" //为版本号添加后缀
buildConfigField "boolean", "IS_INTERNAL_BETA", "true" //设置自定义类型数据,程序中通过 BuildConfig 获取
applicationIdSuffix ".debug" //使应用ID变为 com.zsk.gradledemo.debug
minifyEnabled false // 关闭混淆
manifestPlaceholders = [APP_LOGO_ICON:"@mipmap/ic_test",APP_NAME:"SDK测试版本"] //设置图标与应用程序名
}
release {
versionNameSuffix "-" + apVersionName + "-release" //为版本号添加后缀
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'TPAuthDecoderproguard-rules.pro'
buildConfigField "boolean", "IS_INTERNAL_BETA", "false"
applicationIdSuffix ".release"
manifestPlaceholders = [APP_LOGO_ICON:"@mipmap/ic_release",APP_NAME:"SDK发布版本"]
signingConfig signingConfigs.release // 需要配置签名才能安装
}
dev {
initWith(debug) // 属性继承自 debug
versionNameSuffix "-" + apVersionName + "-dev"
buildConfigField "boolean", "IS_INTERNAL_BETA", "true"
applicationIdSuffix ".dev"
manifestPlaceholders = [APP_LOGO_ICON:"@mipmap/ic_dev",APP_NAME:"SDK开发版本"]
}
}
}

同时需要修改一下 AndroidManifest.xml 中的内容:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zsk.gradledemo" // ①
xmlns:tools="http://schemas.android.com/tools">

<application

android:icon="${APP_LOGO_ICON}" //这里与 buildTypes 中配置的字段保持一致
android:label="${APP_NAME}"
......
</application>

</manifest>

特别注意,在设置 buildTypes 时,若是借助 applicationIdSuffix 属性修改了程序 ID,直接运行有可能会报错,显示找不到类,原因是在 AndroidManifest.xml 文件中少了 ① 处的标识,在之前的版本中是会自动生成的,而这里需要手动添加。

至此在根目录下执行如下指令:

./gradlew assemble

即可得到三个版本的 app:

├── debug
│   ├── app-1.0-2024-07-31-12-43-5b4d275-debug.apk
├── dev
│   ├── app-1.0-2024-07-31-12-43-5b4d275-dev.apk
└── release
├── app-1.0-2024-07-31-12-43-5b4d275-release.apk

现在可以将这三个版本的 app 同时安装到设备中,观察图标、应用名、版本号都是具有各自独特标识的。

4.2 productFlavors

通过 buildTypes 介绍,我们虽然可以做一些自定义的配置,但这种配置更多的为了区分产品的开发周期,对多样化需求就显得力不从心了,因此引入 productFlavors 的概念。productFlavors 通常翻译为 “产品风味”或作 “变体”,这种构建类型是为了满足不同市场和用户需求而设计,更多关注应用程序的外观和行为,例如不同的应用 ID、图标、版本号、应用名等,可以给每个命名不同的渠道、变种应用,提供不同的外观和行为配置。主要致力于为不同用户带来差异化的用户体现,可能这些差异化体现在UI设计/流程/系统版本等

我们来思考这种应用场景,比方说APP的需要对数据流做实时的解析和分发,而数据流的来源有两个,第一是网络通信,第二个是串口输入;那么这种情况下,我们常用的做法是在 APP 中做 if-else 判断,现在我们来借助 productFlavors 来实现另外一种方式:

4.2.1 定义 Flavor

flavorDimensions.add("channel")   // 变体维度
productFlavors {
serialPort {
dimension "channel" //每种变体都归属于一种维度
applicationId "com.zsk.flavor.serialPort"
versionNameSuffix ".serialPort"
versionCode 1
versionName '1.0.2'
manifestPlaceholders = [APP_NAME: "SDK串口版本"]
}
socket {
dimension "channel"
applicationId "com.zsk.flavor.socket"
versionNameSuffix ".socket"
versionCode 1
versionName '1.0.2'
manifestPlaceholders = [APP_NAME: "SDK网络版本"]
}
main {
dimension "channel"
applicationId "com.zsk.flavor.main"
versionNameSuffix ".main"
versionCode 1
versionName '1.0.2'
manifestPlaceholders = [APP_NAME: "SDK主版本"]
}
}

这里解释一下 flavorDimensions 这个属性,在实际需求中可以将产品按照某种分类划分,比如付费版本和免费版本。如果按照渠道划分,比如华为、小米、vivo等。显然这是属于不同的维度,因此就要求定义变体时需要通过 dimension 指定对应的维度,所有的维度都需要事先通过 flavorDimensions 属性来定义。上述示例中仅仅定义了 “channel” 这个一个维度。

创建并配置产品变种后,点击通知栏中的 Sync Now。同步完成后,Gradle 会根据 build 类型和产品变种自动创建 build 变体,并按照 为其命名。例如,我们上面创建了”debug”、”dev”、”release”这三种 build 类型,现在又定义了 “serialPort”、”socket”、”main”三种变体,那么 Gradle 就会创建如下九种 build 变体:

  • mainDebug
  • mainDev
  • mainRelease
  • serialPortDebug
  • serialPortDev
  • serialPortRelease
  • socketDebug
  • socketDev
  • socketRelease

这时可以通过 Android Studio 左下角的 Build Variants 来选择对应的变体安装到设备中。当然也可以通过 ./gradlew assemble 指令一次性将所有的变体编译出来。

4.2.2 定义源码集

不同的变体可能在代码、资源文件等方面有所不同,因此需要指定编译时对应的源码集,根据官网 配置 build 变体中可知定义源码集的方式有两种: 默认方式 和 指定方式

1. 默认方式

Gradle 要求以某种类似于 src/main/源代码集 的方式组织源代码集文件和目录。这样对于我们定义的 serialPort 和 socket 两种变体来说,就需要在 main 的同级目录中创建对应的目录,如 src/serialPort/java 和 src/socket/java 。 对于定义的 main 变体来说就默认使用的 src/main/java 目录下的资源。

活跃的源代码集的图标上会显示绿色指示标志,表明其处于活跃状态。serialPort 源代码集以 [main] 为后缀,表示它将合并到 main 源代码集中。[unitTest] 表示的是测试源代码集,具体可参见官网 测试源代码集 说明,这里不再赘述。

2. 指定方式

如果不想按照默认的形式来定义源代码集的结构,可以在 Android{…} 块中配置 sourceSets 来显示指定,例如将 app/other/ 目录中的源代码映射到 main 源代码集的特定组件,并更改 androidTest 源代码集的根目录:

android {
...
sourceSets {
main {

java.srcDirs = ['other/java']
res.srcDirs = ['other/res1', 'other/res2']
manifest.srcFile("other/AndroidManifest.xml")
...
}

androidTest {

setRoot 'src/tests'
...
}
}
}

默认情况下所有 build 变体之间共享 src/main/源码集和目录,因此常常在 src/main/源代码集 中定义基本功能,在产品变体源代码集中定义不同自己独特的内容,如品牌信息、日志功能、特殊权限等

当我们构建 serialPortDebug 变体时(“serialPort”产品变种和”debug”编译类型的组合产物),Gradle 会查看这些目录,并为它们指定以下优先级:

  1. src/serialPortDebug/(build 变体源代码集)
  2. src/debug/(build 类型源代码集)
  3. src/serialPort/(产品变种源代码集)
  4. src/main/(主源代码集)

优先级顺序决定了 Gradle 组合代码和资源时哪个源代码集的优先级更高。由于 serialPortDebug/ 源代码集目录很可能包含该 build 变体特有的文件,因此如果 serialPortDebug/ 包含的某个文件在 debug/ 中也进行了定义,Gradle 会使用 serialPortDebug/ 源代码集中的文件。同样,Gradle 会为 build 类型和产品变种源代码集中的文件指定比 main/ 中的相同文件更高的优先级。更多可见官网 源代码集构建 介绍。

另外可以看出上面最终的 build 变体是 FlavorbuildType 的组合,其中有一些可能是无意义的,为了减少编译时间,可以采用如下方式进行过滤:

android {
...
buildTypes {...}

flavorDimensions "api", "mode"
productFlavors {
demo {...}
full {...}
minApi24 {...}
minApi23 {...}
minApi21 {...}
}

variantFilter { variant ->
def names = variant.flavors*.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if (names.contains("minApi21") && names.contains("demo")) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
}

4.2.3 仅调整UI布局

现在我们可以具体实现不同变体之间的差异化打包,目录结构如下:

src
├── main
│   ├── AndroidManifest.xml
│   ├── java
│   │   └── com
│   │   └── zsk
│   │   └── gradledemo
│   │   └── MainActivity.java
│   └── res
│   ├── layout
│   │   └── activity_main.xml
├── serialPort
│   └── res
│   └── layout
│   └── activity_main.xml
├── socket
│   └── res
│   └── layout
│   └── activity_main.xml

在 src/main 下创建 MainActivity, 将其中的 activity_main.xml 复制一份放在其他变体之中,并做相应的修改,这样在 MainActivity 中通过 BuildConfig 来作分别做相应的操作:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if ("serialPort".equals(BuildConfig.FLAVOR)){
...
} else if ("socket".equals(BuildConfig.FLAVOR)) {
...
} else {
...
}
}
}

当编译不同的变体时,BuildConfig.FLAVOR 就会发生变化接着就执行对应的逻辑,但这种形式有一种限制,就是要求这些 build 变体中的 activity_main.xml 界面元素完全一样,比方说 TextView 的id,数量等,也就是说界面能改变的仅仅是布局!

4.2.4 启动不同页面

我们想一下上面的优先级,现在的 MainActivity 是定义在 src/main/源代码集 中的,这个源代码集是默认被所有的 build 变体共享和集成的,因此这里我们直接将 src/main/ 下的 MainActivity 删除,并将对应的清单文件中的启动页面删除,将启动页面完全放到变体中即可实现:

src
├── main
│   ├── AndroidManifest.xml
│   ├── java
│   │   └── com
│   │   └── zsk
│   │   └── gradledemo
│   └── res
│   ├── drawable
│   │   ├── ic_launcher_background.xml
│   │   └── ic_launcher_foreground.xml
│   ├── layout

├── serialPort
│   ├── AndroidManifest.xml
│   ├── java
│   │   └── com
│   │   └── zsk
│   │   └── gradledemo
│   │   └── FlavorActivity.java
│   └── res
│   ├── layout
│   │   ├── activity_flavor.xml
│   └── values
│   └── strings.xml
├── socket
│   ├── AndroidManifest.xml
│   ├── java
│   │   └── com
│   │   └── zsk
│   │   └── gradledemo
│   │   └── FlavorActivity.java
│   └── res
│   ├── layout
│   │   └── activity_flavor.xml
│   └── values
│   └── strings.xml

其中 src/main/AndroidManifest.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.zsk.gradledemo">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="${APP_LOGO_ICON}"
android:label="${APP_NAME}"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GradleDemo">

</application>

</manifest>

src/serialPort/AndroidManifest.xml 如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="com.zsk.gradledemo.FlavorActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

src/socket/AndroidManifest.xml 如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="com.zsk.gradledemo.FlavorActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

4.2.5 声明依赖项

若要为特定 build 变体或测试源代码集配置依赖项,可以在 Implementation 关键字前面加上 build 变体或测试源代码集的名称作为前缀,如以下示例所示:

dependencies {
// Adds the local "mylibrary" module as a dependency to the "free" flavor.
freeImplementation project(":mylibrary")

// Adds a remote binary dependency only for local tests.
testImplementation 'junit:junit:4.12'

// Adds a remote binary dependency only for the instrumented test APK.
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.5.1'
}

如需详细了解如何配置依赖项,请参阅添加 build 依赖项

//