在我的開發中,有這樣的需求,有一個項目,需要適配不同的執法儀設備,這些執法儀都是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"]}head {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(".hello") -> 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類型。