一、简介 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 API 中 com.android.build.api.dsl 这个包下查看。下面我们分别从这个项目中的几个构建脚本入手,来对 AGP 的构建属性展开介绍;
3.1 settings.gradle pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } 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 :
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 相关属性的了解,可以通过官网开放的文章 配置build 和 Gradle 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 模块下的有一些不同,表现在如下两点,除此之前相关配置内容都大同小异:
使用 library 插件,表明当前 module 需要编译为 aar 或 jar 等库文件
删去 applicationId 标识
四、常用示例 4.1 buildTypes 这种构建类型的设计主要是为了区分开发周期中的不同阶段,比如 debug/test/pre/release 等。这些构建类型在功能上对用户来说可能没有太大区别,但它们在实际应用中扮演着不同的角色。例如,调试版本可能会打印日志或执行调试代码,而发布版本则更加注重性能和安全性,可能不包含调试信息或进行了代码混淆。buildTypes 还涉及到应用的签名配置、混淆文件的设置等,确保应用在不同构建类型下具有适当的配置和优化。
现在我们来实现这样一个功能,要求同时输出 dev/debug/release/ 版本,且版本号带有相应标识以及具有不同的应用 ID、应用名称、应用图标:
import java.text.SimpleDateFormatplugins { 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() debug { versionNameSuffix "-" + apVersionName + "-debug" buildConfigField "boolean" , "IS_INTERNAL_BETA" , "true" applicationIdSuffix ".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) 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}" android: label="${APP_NAME}" ...... </application> </manifest>
特别注意,在设置 buildTypes 时,若是借助 applicationIdSuffix 属性修改了程序 ID,直接运行有可能会报错,显示找不到类,原因是在 AndroidManifest.xml 文件中少了 ① 处的标识,在之前的版本中是会自动生成的,而这里需要手动添加。
至此在根目录下执行如下指令:
即可得到三个版本的 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 会查看这些目录,并为它们指定以下优先级:
src/serialPortDebug/(build 变体源代码集)
src/debug/(build 类型源代码集)
src/serialPort/(产品变种源代码集)
src/main/(主源代码集)
优先级顺序决定了 Gradle 组合代码和资源时哪个源代码集的优先级更高。由于 serialPortDebug/ 源代码集目录很可能包含该 build 变体特有的文件,因此如果 serialPortDebug/ 包含的某个文件在 debug/ 中也进行了定义,Gradle 会使用 serialPortDebug/ 源代码集中的文件。同样,Gradle 会为 build 类型和产品变种源代码集中的文件指定比 main/ 中的相同文件更高的优先级。更多可见官网 源代码集构建 介绍。
另外可以看出上面最终的 build 变体是 Flavor 和 buildType 的组合,其中有一些可能是无意义的,为了减少编译时间,可以采用如下方式进行过滤:
android { ... buildTypes {...} flavorDimensions "api" , "mode" productFlavors { demo {...} full {...} minApi24 {...} minApi23 {...} minApi21 {...} } variantFilter { variant -> def names = variant.flavors*.name if (names.contains("minApi21" ) && names.contains("demo" )) { 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 { freeImplementation project(":mylibrary" ) testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.5.1' }
如需详细了解如何配置依赖项,请参阅添加 build 依赖项 。