概述
Andorid Gradle支持一些有用的功能,比方:躲藏签名证书文件,降低证书露出的危险。批量修正apk的称号,让称号一眼就能看出途径,版别号,生成日期等要害信息。
1. 批量修正生成的apk称号
apk文件作为 AndroidGradle打包的终究产品,修正它的称号,其实便是修正输出产品的流程。而,Android Gradle插件中,有一个android对象,也便是下面的:
android {
compileSdkVersion rootProject.ext.compileSdkVerion
...
}
它为咱们供给了 3个有用的特点,application Variants (运用变体),libraryVariant(库变体), testVariants(测验变体)
这3个变体回来的都是一个对象调集,调集的类型是:DomainObjectSet , 就apk生成的流程来说,它受到 buildTypes (构件类型) 和 Product Flavors(多途径设置) 的影响。
一般咱们经过迭代拜访这些调集的元素,就能达成修正终究产品称号的目的,当然这儿说的是修正apk文件名,那么针对性的便是在 拜访 application Variants 。
以下是示例代码,这段代码以多flavor途径,多buildType装备的情况下,会产生很多个variant,咱们获取到每一个variant之后,能修正它的output文件,此时乃至能够修正它的文件途径。
每个gradle版别的写法都有可能不同,某些特点可能被移除,以下是 gradle-5.6.4 的写法:
import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 16
targetSdkVersion 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
flavorDimensions "channel"
productFlavors {
flavor1 {
dimension "channel"
// 装备其他自界说特点
}
flavor2 {
dimension "channel"
// 装备其他自界说特点
}
// 增加更多的途径装备
}
applicationVariants.all { variant ->
variant.outputs.all { output ->
def variantName = variant.name.capitalize()
def versionName = variant.versionName
def versionCode = variant.versionCode
def apkDirectory = output.outputFile.parentFile
// 获取当时日期时刻
def timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date())
// 是否签名
def isSigned = variant.signingConfig != null
def signedText = isSigned ? "signed" : "unsigned"
def apkName = "myProjectName_${timeStamp}_v${versionName}_(${versionCode})_${variantName}_${signedText}.apk"
// 修正输出途径和文件名
output.outputFileName = new File(apkDirectory, apkName).name
}
}
}
当然,咱们apk的输出文件名形如: myProjectName_20230722_165242_v1.0_(1)_Flavor1Release_unsigned.apk
,
自始至终,包括了 以下首要元素:
- 项目名
myProjectName
- 日期时刻
20230722_165242
- 版别称号和版别号
v1.0_(1)
- 变体名
Flavor1Release
- 是否签名
unsigned
2. 动态生成版别信息
运用的版别号 一般由3个部分组成,major.minor.path
,例如:1.0.0.当然也有短号 major.minor
,例如:1.0. 一般以前者较为主流。
开发中常常遇到的一个情况,打测验包,要常常修正bug后从头打包,为了防止运用无法安装,咱们经过gradle装备,将出产包和测验包进行分隔处理。比方,咱们先界说2个buildType(release和uat),uat 包咱们需要在每次修正bug后更新版别号,而出产包则必须读取 大局的版别号装备。uat的包还需要从git提交记载中提取最后一次修正的 提交者名字和批次名,用于确定本次打包时用的是何时的代码,削减与测验搭档交流过程中的无效交流。
出产和测验包分隔两套versionCode/versionName
针对上述要求,咱们规划了如下Gradle装备, 新增一个叫做uat的buildType
,而且复制自 debug buildType
。一切用uat打出的包,运用固定的versionCode,以及动态的versionName(时刻日期组成)。而用release打出的包,都运用正式的版别号和版别号。
gradle具体装备如下:
import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
ext {
releaseVersionName = "2.1.3"
appVersionCode = 213
debugAppVersionCode = 99999
}
android {
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 16
targetSdkVersion 31
versionCode getVersionCode(true)
versionName getVersionName(true)
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
uat {
// 复制 debug 装备
initWith debug
debuggable false
}
}
// 商场途径
flavorDimensions "channel"
productFlavors {
google {
dimension "channel"
// 装备其他自界说特点
}
huawei {
dimension "channel"
// 装备其他自界说特点
}
// 增加更多的途径装备
}
applicationVariants.all { variant ->
variant.outputs.all { output ->
def variantName = variant.name.capitalize()
def versionName
def versionCode
if (variantName.contains("Release")) {
versionName = getVersionName(true)
versionCode = getVersionCode(true)
output.versionCodeOverride = versionCode
output.versionNameOverride = versionName
} else if (variantName.contains("Uat")) {
versionName = getVersionName(false)
versionCode = getVersionCode(false)
output.versionCodeOverride = versionCode
output.versionNameOverride = versionName
}
println("variantName -> ${variantName} -> ${versionName} -> ${versionCode}")
def apkDirectory = output.outputFile.parentFile
// 获取当时日期时刻
def timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date())
// 是否签名
def isSigned = variant.signingConfig != null
def signedText = isSigned ? "signed" : "unsigned"
def apkName = "myProjectName_${timeStamp}_v${versionName}(${versionCode})_${variantName}_${signedText}.apk"
// 修正输出途径和文件名
output.outputFileName = new File(apkDirectory, apkName).name
}
}
}
def getVersionName(boolean isRelease) {
// 正式环境
if (isRelease) {
releaseVersionName
}
// debug环境
else {
String today = new Date().format("MMdd")
String time = new Date().format("HHmm")
"${releaseVersionName}.$today.$time"
}
}
def getVersionCode(boolean isRelease) {
if (isRelease) {// 正式环境
appVersionCode
} else {// debug环境
debugAppVersionCode
}
}
dependencies {
...
}
经过 assembleGoogleUat打出的包,文件名为:myProjectName_20230722_190601_v2.1.3.0722.1906(99999)_GoogleUat_signed.apk
, 安装apk之后查看版别号,发现与预期的版别号也对的上。
而经过 assembleGoogleRelease打出包,文件名为:myProjectName_20230722_191223_v2.1.3(213)_GoogleRelease_unsigned.apk
以git提交记载为数据源提取versionName和versionCode
git中存在这么一个指令:git describe --abbrev=0 --tags
它用于获取git库房上的最近一个tag称号。还有一个指令,git tag --list
, 它用于获取当时git库房的一切tag标签。
假如咱们能经过gradle脚本执行这两个指令,经过前者获取的tag称号,界说为动态的versionName,经过后者获取tag的list,再取它的size,界说为versionCode。那么咱们在发新版别完成之后,就只需要打一个新的tag,打出的出产保便是运用的最新的tag称号,build.gradle中的versionCode和VersionName则不必再随着发版别而变动。
实操
很多groovy脚本与上一末节出产和测验包分隔两套versionCode/versionName
是重复的,所以本节只列出要害代码:
def getAppVersionName(){
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git','describe','--abbrev=0','--tags'
standardOutput = stdout
}
return stdout.toString()
}
def getAppVersionCode(){
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git','tag','--list'
standardOutput = stdout
}
return stdout.toString().split("\n").size
}
解释
exec 是 gradle的project对象 供给的一个办法,能够当做大局函数来运用。一般用一个闭包作为它的参数,这个闭包是经过 ExecSpec来装备的,ExecSpec内部有一个 commandLine 特点用来装备 指令的各个部分,standardOutput则表明将指令的执行结果输出给哪个输出流。
3. 躲藏签名文件信息
签名文件一般被存储在项目的根目录下的keystore
文件中,并在build.gradle
文件中装备签名信息。然而,由于签名文件包括灵敏的证书信息,不该该被揭露或无意走漏。 一个开发组中一般由某个leader持有真实的签名文件来打出终究的出产包,而其他人仅有开发权限以及打出测验包的权限,测验包不具有真是签名,所以能够确保签名文件不走漏。 这也是确保安全出产的场景之一。
计划1
以下过程能够构建一套 签名文件安全保存的开发环境。
-
在本机增加体系环境变量,设置签名所需4个参数:签名文件途径ANDROID_KEYSTORE_PATH,ANDROID_STORE_PASSWORD 暗码,ANDROID_KEYALIAS 别号,ANDROID_KEYALIAS_PWD,别号暗码。
-
Gradle中读取环境变量来装备签名
def env = System.getenv() def keystorePath = env['ANDROID_KEYSTORE_PATH'] def keystorePwd = env['ANDROID_STORE_PASSWORD'] def keyAliasName = env['ANDROID_KEYALIAS'] def keyAliasPwd = env['ANDROID_KEYALIAS_PWD'] signingConfigs { release { storeFile file(keystorePath) storePassword keystorePwd keyAlias keyAliasName keyPassword keyAliasPwd } }
-
签名文件仅团队leader持有,其他人假如测验打release包,则提示,gradle编译异常,呈现签名文件找不到的错误。
计划2
签名文件不放在leader本机环境变量,而是加密后放在项目中随git提交。一起为了确保签名文件的安全,leader保存仅有一份解密密钥。
具体过程如下:
-
要害要素加密
由于是文件等级的加密,所以,优先运用AES进行加密(确保加解密的功率),然后将 加密后的签名文件进行保存,一起将对称加密的密钥,经过RSA加密(确保密钥的安全)。
此时,有几个要害要素要明确:
称号 说明 是否随git提交 签名文件原件 不随git提交 否 AES的密钥 用于给签名文件原件AES加密 否 RSA的公钥和私钥 用于给“AES密钥” 进行加密 否 签名文件的密件 经过 对称加密处理过的签名文件 是 RSA加密后的AES密钥 此密钥,必须经过RSA解密后才干用于 处理签名文件密件 是 项目中,可随git提交的,都是加密处理后的文件或字符串,要进行release打包,则只要leader所独有的 RSA私钥,才干使得打包流程走通。
-
gitignore设置
依照上述表格,设置好 gitignore。
-
自界说解密Task,在检测到release打包时,运用本地的RSA私钥来解密
AES密钥的密文
, 然后运用AES密钥来对 签名文件密件进行解密,并将解密后的原件存在项目的固定位置。 -
signingConfigs 装备
release装备中的 storeFile 对应上一过程中的签名文件原件。其他参数可随git提交,不影响安全(?)
signingConfigs { release { storeFile file(keystorePath) storePassword keystorePwd keyAlias keyAliasName keyPassword keyAliasPwd } }
-
当执行release打包任务结束之后,删除签名文件的原件。
同样,在其他组员测验打出release包时,则会提示,签名文件找不到。
4. 自界说BuildConfig
基本办法
Gradle生成终究产品apk的过程中,其间一个过程便是 生成buildConfig文件。而这个文件的内容来源,一部分便是来自Gradle装备。
android {
...
defaultConfig {
...
// 自界说常量
buildConfigField "String", "API_KEY", ""your_api_key""
}
}
上面的代码会向BuildConfig类增加一个名为API_KEY的常量,它的值为”your_api_key”。咱们能够在代码中经过BuildConfig.API_KEY拜访这个常量。
一般咱们会挑选将一部分参数用于区别 当时app的打包环境(release出产,或者uat测验),不同环境下的 app包名,版别号,版别号,API_KEY等,都能够做区别。
除了以上的字符串类型之外,还能够界说如下类型:
-
整数常量:
buildConfigField "int", "VERSION_CODE", "10"
这会在BuildConfig中增加一个名为VERSION_CODE的整数常量,其值为10。
-
布尔值常量:
buildConfigField "boolean", "ENABLE_FEATURE", "true"
这会在BuildConfig中增加一个名为ENABLE_FEATURE的布尔值常量,其值为true。
-
浮点数常量:
buildConfigField "float", "PI_VALUE", "3.14f"
这会在BuildConfig中增加一个名为PI_VALUE的浮点数常量,其值为3.14。
-
长整数常量:
buildConfigField "long", "MAX_SIZE", "100000000L"
这会在BuildConfig中增加一个名为MAX_SIZE的长整数常量,其值为100000000。
-
字符常量:
buildConfigField "char", "LOG_LEVEL", "'D'"
这会在BuildConfig中增加一个名为LOG_LEVEL的字符常量,其值为’D’。
优化计划
假如一个项目中存在很多个 buildConfigField,特别是字符串特别多的时分,导致gradle文件看起来很乱。咱们能够经过对字符串进行抽离并进行外部存储,gradle中进行读取的方法进行优化。
1. 运用自界说gradle脚本
将一些常用的buildConfigField移动到独自的gradle脚本文件中。例如,你能够创立一个名为 build_config.gradle
的文件,然后在主build.gradle文件中引进它:
apply from: "build_config.gradle"
在 build_config.gradle
文件中,能够界说一切的buildConfigField:
ext {
appVersion = "1.0"
apiKey = "your_api_key"
maxItemCount = 100
// 其他buildConfigFields...
}
android {
defaultConfig {
// 运用自界说常量
buildConfigField "String", "APP_VERSION", ""${appVersion}""
buildConfigField "String", "API_KEY", ""${apiKey}""
buildConfigField "int", "MAX_ITEM_COUNT", "${maxItemCount}"
// 其他buildConfigFields...
}
}
这样,你能够将一切的buildConfigField集中在一个独自的脚本中,使主build.gradle文件更加清晰,易于维护。
2. 运用额定的构建变量文件
除了运用自界说gradle脚本外,你也能够运用额定的构建变量文件,例如 build_vars.properties
。在这个文件中,你能够界说一切的buildConfigField:
APP_VERSION=1.0
API_KEY=your_api_key
MAX_ITEM_COUNT=100
然后,在build.gradle文件中读取这些变量并运用它们:
def buildVars = new Properties()
buildVars.load(new FileInputStream(file('build_vars.properties')))
android {
defaultConfig {
// 运用自界说常量
buildConfigField "String", "APP_VERSION", ""${buildVars['APP_VERSION']}""
buildConfigField "String", "API_KEY", ""${buildVars['API_KEY']}""
buildConfigField "int", "MAX_ITEM_COUNT", "${buildVars['MAX_ITEM_COUNT']}"
// 其他buildConfigFields...
}
}
这样,你能够将一切的buildConfigField集中在一个独自的文件中,而且能够经过编辑这个文件来调整常量的值,而不需要修正build.gradle文件。
这些办法能够协助你将代码整理得更加优雅和易于维护,削减build.gradle文件的复杂度,并防止在文件中呈现大量的buildConfigField声明。挑选适合你项目需求的办法,以进步代码的可读性和可维护性。一起,假如buildConfigField过火复杂,还能够利用上面两种方法,将buildConfigField抽离为多个外部文件,依照特征进行分批次保存,进一步进步代码的可读性。