在我的开发中,有这样的需求,有一个项目,需要适配不同的执法仪设备,这些执法仪都是Android系统的,而且有的有系统签名,有的没有,比如我共有四款型号,有三款有系统签名,每款系统签名各不一样,有一款无系统签名,总结就是我需要使用4个不同签名用到4个型号上,这就必须要有4个apk,因为一个apk不可能同时拥有4个不同签名,所以就会导致有如下需求:
- 生成4个apk,每个apk的签名不相同,签名不相同导致应用ID(包名)也不能相同。
- 使用系统签名的需要在清单文件中设置
android:sharedUserId="android.uid.system"
,不使用系统签名的则不设置。 - 4个apk的版本号可能不一样,所以需要分别设置版本信息。
- 有一款号型是只支持32位CPU的,对应只能使用32位的so,其它的使用64位so。
最开始我是使用变量来表示各种版本和配置,但是每打包一个版本时,就需要修改变量,比如把flag设置为1,对应的配置使用为型号1的配置,然后还要经常修改清单文件,这很麻烦,所以,这时候flavor
就派上了用场,可以节省许多宝贵时间。
为4个签名文件设置对应的配置(下面的配置均使用Groovy语言):
android {
signingConfigs {
/** 型号1,使用系统签名 */
normal {
keyAlias 'aaa'
keyPassword 'aaa'
storeFile file('aaa.keystore')
storePassword 'aaa'
}
/** 型号2,使用系统签名 */
head {
keyAlias 'bbb'
keyPassword 'bbb'
storeFile file('bbb.keystore')
storePassword 'bbb'
}
/** 型号3,使用系统签名 */
hand {
keyAlias 'ccc'
keyPassword 'ccc'
storeFile file('ccc.jks')
storePassword 'ccc'
}
/** 型号4,使用普通签名 */
hik {
keyAlias 'ddd'
keyPassword 'ddd'
storeFile file('ddd.jks')
storePassword 'ddd'
}
}
}
然后根据需求配置flavor
:
android {
flavorDimensions "version"
productFlavors {
normal {
dimension "version"
versionCode 202508180
versionName "1.1.0"
// 应用id没指定,则和原来的保持一样
signingConfig signingConfigs.normal
// 使用32位so
ndk.abiFilters "armeabi-v7a"
// 指定清单文件中的sharedUserId
manifestPlaceholders = [sharedUid: "android.uid.system"]
}
head {
dimension "version"
versionCode 202508080
versionName "1.0.0"
applicationIdSuffix ".head" // 修改应用ID,在原来包名基础上添加.head
signingConfig signingConfigs.head
ndk.abiFilters "arm64-v8a"
// 指定清单文件中的sharedUserId
manifestPlaceholders = [sharedUid: "android.uid.system"]
}
hand {
dimension "version"
versionCode 202508110
versionName "1.0.0"
applicationIdSuffix ".hand" // 修改应用ID,在原来包名基础上添加.hand
signingConfig signingConfigs.hand
ndk.abiFilters "arm64-v8a"
// 指定清单文件中的sharedUserId
manifestPlaceholders = [sharedUid: "android.uid.system"]
}
hik {
dimension "version"
versionCode 202508180
versionName "1.0.0"
applicationIdSuffix ".hik" // 修改应用ID,在原来包名基础上添加.hik
signingConfig signingConfigs.hik
ndk.abiFilters "arm64-v8a"
// 指定清单文件中的sharedUserId,设置为空即为普通应用,不使用系统签名的
manifestPlaceholders = [sharedUid: ""]
}
}
}
从这里可以看到,通过flavor
,我们可以很方便的给每个变体设置不一样的版本号、应用ID、签名、so、sharedUserId等,flavor支持的配置远不止这些,如果你还有更多配置需要,自行问AI即可。
这里第一个flavor我们没有配置应用ID,则它和默认的保持一样,比如:
android {
defaultConfig {
applicationId "com.example.hello"
}
}
其它的flavor则添加了后缀,比如:applicationIdSuffix ".hik"
,则它实际使用的应用ID为:com.example.hello.hik
。按道理每个flavor都添加后缀比较好看一点,为什么第一个我没添加,这是因为在做这一款型号的开发的时候,我不知道它有这么多型号,所以当时就使用了com.example.hello
包名,且已经上线了,后来来了几款型号说也要适配,所以此时这个包名已经是不能修改的了。
还有这里指定的manifestPlaceholders = [sharedUid: "android.uid.system"]
,它会自动注入清单文件,清单文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:sharedUserId="${sharedUid}">
这里需要注意的是,flavor中指定的签名配置只对release版本生效,对于系统签名,即使是debug版本,我们也希望使用系统签名,因为有些api,必须使用系统签名才能调用的,如果debug版本使用了Android Studio自带的debug.keystore,则会抛出异常,所以我们可以配置不使用自带的debug.keystore,如下:
android {
buildTypes {
debug {
// 注:这里的签名配置会覆盖productFlavors中设置的签名配置,所以要想使用productFlavors中配置的签名,则这里不能配置签名
// debug签名,即使我们不配置signingConfig,但它默认其实是配置了使用Android默认的debug.keystore签名的,所以要想debug的变体
// 也使用productFlavors中配置的签名,则需要在这里手动把signingConfig设置为null,这样构造debug变体时才会使用productFlavors中的签名。
minifyEnabled false
signingConfig null // 禁用默认签名
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
flavor配置好之后,开发就简单了许多,比如当我需要开发hik版本时,我就在构建变体中选择hik版本即可,然后调试的时候就直接点运行按钮,则hik的Debug版本就会运行到设备上,如下:
当需要打包某个版本时,直接使用gradle命令,我们可以先在gradle面板中执行tasks命令来查看当前项目都有哪些命令,如下:
如上图,在右上角选择我们的app模块(不选择其实也没问题,选择了就更好一些,表明只看app模块的可用任务),然后在输入框中输入tasks然后回车,结果如下:
在Build tasks
分组下,assemble
开头的命令则为打包apk的命令:
命令 | 构建范围 | 输出数量 | 典型用途 |
---|---|---|---|
assemble |
所有风味 × 所有构建类型 | 8个APK | 全设备全版本打包(CI/CD) |
assembleDebug |
所有风味 × Debug |
4个APK | 所有设备的调试测试版本 |
assembleRelease |
所有风味 × Release |
4个APK | 所有设备的正式发布版本 |
assembleHead |
head风味 × 所有构建类型 | 2个APK | 特定设备的调试+正式版本 |
其实tasks任务并没有完全打印所有的assemble
命令,比如我就想打包一个hik
风味的release
版本,则可以用:assembleHikRelease
,如果只要debug,则为assembleHikDebug
。
总结就是:assemble
可单独使用,也可加风味,也可加构建类型,也可都加,在输入命令时,这太长了又容易输错,所以可以使用缩写,比如我要打包风味为normal
的release
版本,完整命令为:assembleNormalRelease
,缩写为aNR
,对于Head
、Hand
、Hik
,它们都是H
开头,所以可以再加多第二个字母来区别,比如要打包Hik
的release
版本,则可以用:aHiR
。
不知道是不是我的Android插件版本不对,我执行assemble命令生成的apk位置在app/build/intermediates/apk
目录下,截图如下:
执法assemble命令来打包所有版本时,也是可以用缩写的,截图如下:
生成所有debug版本:gradle aD
,这与androidDependencies
冲突了,则改用:gradle asD
,反正不用记,先执行,冲突了会报错,然后再改了再执行即可,效果如下:
生成所有release版本:gradle aR
,效果如下:
生成hik风味的debug与release版本:gradle aHi
,效果如下:
生成hik风味的release版本:gradle aHiR
,效果如下:
生成hik风味的debug版本:gradle aHiD
,效果如下:
有时候在代码中,还需要根据变体做特殊处理,比如我的某个变体使用普通签名,则它不能调用那些需要系统签名的API,在代码中判断当前是哪个变体也很简单,我们是给应用ID添加的后缀,则判断后缀即可,如下:
class MyApplication : Application() {
companion object {
var isNormal = false
var isHead = false
var isHand = false
var isHik = false
}
fun onCreate() {
when {
packageName.endsWith(".hello") -> isNormal = true
packageName.endsWith(".head") -> isHead = true
packageName.endsWith(".hand") -> isHand = true
packageName.endsWith(".hik") -> isHik = true
}
}
}
flavor的一个经典应用就是同一个项目提供免费版本和付费版本,也可以理解为基础版本和高级版本,高级版本需要收费。由于近年来kotlin语言做为build.gradle.kts语言越来越流行了,所以下面使用kotlin语言进行示例演示:
android {
flavorDimensions += "version"
productFlavors {
create("free") {
dimension = "version"
applicationId = "cn.android666.audiorecorder.free"
versionCode = 1
versionName = "1.0-free"
}
create("paid") {
dimension = "version"
applicationId = "cn.android666.audiorecorder.paid"
versionCode = 1
versionName = "1.0-paid"
}
}
}
在flavor配置中,还可以为Debug和Release分别设置服务器IP、端口等,这样通过切换变体就能实现服务器的切换,无需要手动修改。假设免费版和收费版使用的服务器IP和端口都是一样的,但是debug版本和release版本不一样,其实这种情况就只和构建类型相关,和flavor不相关了,所以在构建类型中定义即可,如下:
android {
buildTypes {
debug {
// Debug版本使用公司内部服务器
buildConfigField("String", "SERVER_IP", "\"192.168.10.100\"")
buildConfigField("int", "SERVER_PORT", "3000")
}
release {
// Release版本使用生产环境服务器
buildConfigField("String", "SERVER_IP", "\"47.98.123.156\"")
buildConfigField("int", "SERVER_PORT", "80")
}
}
buildFeatures {
buildConfig = true
}
}
假设情况有变了,debug版本和release版本的服务器ip端口是一样的,只是免费版本和付费版不相同,这就跟构建类型不相关了,而是跟flavor相关了,所以就不要在构建类型中配置ip和端口了,而应该以在flavor中配置,如下:
android {
flavorDimensions += "version"
productFlavors {
create("free") {
dimension = "version"
applicationId = "cn.android666.audiorecorder.free"
versionCode = 1
versionName = "1.0-free"
// 免费版服务器配置
buildConfigField("String", "SERVER_IP", "\"47.102.56.122\"")
buildConfigField("int", "SERVER_PORT", "3000")
}
create("paid") {
dimension = "version"
applicationId = "cn.android666.audiorecorder.paid"
versionCode = 1
versionName = "1.0-paid"
// 付费版服务器配置
buildConfigField("String", "SERVER_IP", "\"47.102.56.123\"")
buildConfigField("int", "SERVER_PORT", "8080")
}
}
buildFeatures {
buildConfig = true
}
}
假设情况又有变了,对于免费版本和付费版本,它们分别使用不同的服务器,且它们的debug版本和release版本也是使用不同的服务器,此时不但和构建类型相关,还和和flavor相关,这种情况属于flavor和构建类型相交差的情形,声明在构建类型配置中不合适,声明在flavor配置中也不合适,这需要动态设置,示例如下:
android {
buildTypes {
debug {
isMinifyEnabled = false
}
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
flavorDimensions += "version"
productFlavors {
create("free") {
dimension = "version"
applicationId = "cn.android666.audiorecorder.free"
versionCode = 1
versionName = "1.0-free"
}
create("paid") {
dimension = "version"
applicationId = "cn.android666.audiorecorder.paid"
versionCode = 1
versionName = "1.0-paid"
}
}
applicationVariants.all {
val variant = this
var serverIp = "\"192.168.1.100\"" // 默认服务器
var serverPort = "8080" // 默认端口
// 根据变体名称配置不同的服务器IP和端口
when (variant.name) {
"freeDebug" -> {
serverIp = "\"192.168.192.128\"" // 免费版调试服务器
serverPort = "3000" // 免费版调试端口
}
"freeRelease" -> {
serverIp = "\"47.98.123.156\"" // 免费版生产服务器
serverPort = "80" // 免费版生产端口
}
"paidDebug" -> {
serverIp = "\"192.168.192.100\"" // 付费版调试服务器
serverPort = "4000" // 付费版调试端口
}
"paidRelease" -> {
serverIp = "\"47.102.56.123\"" // 付费版生产服务器
serverPort = "8080" // 付费版生产端口
}
}
variant.buildConfigField("String", "SERVER_IP", serverIp)
variant.buildConfigField("int", "SERVER_PORT", serverPort)
}
buildFeatures {
buildConfig = true
}
}
在代码中访问服务器IP和端口:
Log.i("TAG", "Server IP: ${BuildConfig.SERVER_IP}, Port: ${BuildConfig.SERVER_PORT}")
运行不同的变体就能得到不同的服务器IP和端口,无需每次都手动修改代码,这样大大节省了宝贵时间。
这里需要注意的是,我们在build.gradle.kts中指定的int类型时不要设置为Int,如下:
variant.buildConfigField("Int", "SERVER_PORT", serverPort)
这样生成的BuildConfig.java
代码如下:
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "cn.android666.audiorecorder.free";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "free";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0-free";
// Field from the variant API
public static final String SERVER_IP = "192.168.192.128";
// Field from the variant API
public static final Int SERVER_PORT = 3000;
}
虽然语法上是错的,但是它还是生成了,这是Java代码,不是Kotlin,Java中是没有Int类型的,只有小写的int,有时候不注意,用kotlin习惯了,一下子转不过来,明明Int生成了,但是为什么使用的时候报错,如下:
报错原因就是Java中没有Int
类型只有int
类型。