Android中flavor的使用
在我的开发中,有这样的需求,有一个项目,需要适配不同的执法仪设备,这些执法仪都是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 202508180versionName "1.1.0"// 应用id没指定,则和原来的保持一样signingConfig signingConfigs.normal// 使用32位sondk.abiFilters "armeabi-v7a"// 指定清单文件中的sharedUserIdmanifestPlaceholders = [sharedUid: "android.uid.system"]}ylxHead {dimension "version"versionCode 202508080versionName "1.0.0"applicationIdSuffix ".head" // 修改应用ID,在原来包名基础上添加.headsigningConfig signingConfigs.headndk.abiFilters "arm64-v8a"// 指定清单文件中的sharedUserIdmanifestPlaceholders = [sharedUid: "android.uid.system"]}hand {dimension "version"versionCode 202508110versionName "1.0.0"applicationIdSuffix ".hand" // 修改应用ID,在原来包名基础上添加.handsigningConfig signingConfigs.handndk.abiFilters "arm64-v8a"// 指定清单文件中的sharedUserIdmanifestPlaceholders = [sharedUid: "android.uid.system"]}hik {dimension "version"versionCode 202508180versionName "1.0.0"applicationIdSuffix ".hik" // 修改应用ID,在原来包名基础上添加.hiksigningConfig signingConfigs.hikndk.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 falsesigningConfig null // 禁用默认签名proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}release {minifyEnabled falseproguardFiles 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 = falsevar isHead = falsevar isHand = falsevar isHik = false}fun onCreate() {when {packageName.endsWith(".bj") -> isNormal = truepackageName.endsWith(".head") -> isHead = truepackageName.endsWith(".hand") -> isHand = truepackageName.endsWith(".hik") -> isHik = true}}
}
flavor的一个经典应用就是同一个项目提供免费版本和付费版本,也可以理解为基础版本和高级版本,高级版本需要收费。由于近年来kotlin语言做为build.gradle.kts语言越来越流行了,所以下面使用kotlin语言进行示例演示:
android {flavorDimensions += "version"productFlavors {create("free") {dimension = "version"applicationId = "cn.android666.audiorecorder.free"versionCode = 1versionName = "1.0-free"}create("paid") {dimension = "version"applicationId = "cn.android666.audiorecorder.paid"versionCode = 1versionName = "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 = 1versionName = "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 = 1versionName = "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 = falseproguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")}}flavorDimensions += "version"productFlavors {create("free") {dimension = "version"applicationId = "cn.android666.audiorecorder.free"versionCode = 1versionName = "1.0-free"}create("paid") {dimension = "version"applicationId = "cn.android666.audiorecorder.paid"versionCode = 1versionName = "1.0-paid"}}applicationVariants.all {val variant = thisvar 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 APIpublic static final String SERVER_IP = "192.168.192.128";// Field from the variant APIpublic static final Int SERVER_PORT = 3000;
}
虽然语法上是错的,但是它还是生成了,这是Java代码,不是Kotlin,Java中是没有Int类型的,只有小写的int,有时候不注意,用kotlin习惯了,一下子转不过来,明明Int生成了,但是为什么使用的时候报错,如下:
报错原因就是Java中没有Int
类型只有int
类型。