Android中flavor的使用

发布于:2025-08-19 ⋅ 阅读:(17) ⋅ 点赞:(0)

在我的开发中,有这样的需求,有一个项目,需要适配不同的执法仪设备,这些执法仪都是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可单独使用,也可加风味,也可加构建类型,也可都加,在输入命令时,这太长了又容易输错,所以可以使用缩写,比如我要打包风味为normalrelease版本,完整命令为:assembleNormalRelease,缩写为aNR,对于HeadHandHik,它们都是H开头,所以可以再加多第二个字母来区别,比如要打包Hikrelease版本,则可以用: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类型。


网站公告

今日签到

点亮在社区的每一天
去签到