概述

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 != nulldef 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 != nulldef 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

以下过程能够构建一套 签名文件安全保存的开发环境。

  1. 在本机增加体系环境变量,设置签名所需4个参数:签名文件途径ANDROID_KEYSTORE_PATH,ANDROID_STORE_PASSWORD 暗码,ANDROID_KEYALIAS 别号,ANDROID_KEYALIAS_PWD,别号暗码。

  2. 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
       }
     }
    
  3. 签名文件仅团队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等,都能够做区别。

除了以上的字符串类型之外,还能够界说如下类型:

  1. 整数常量:

    buildConfigField "int", "VERSION_CODE", "10"
    

    这会在BuildConfig中增加一个名为VERSION_CODE的整数常量,其值为10。

  2. 布尔值常量:

    buildConfigField "boolean", "ENABLE_FEATURE", "true"
    

    这会在BuildConfig中增加一个名为ENABLE_FEATURE的布尔值常量,其值为true。

  3. 浮点数常量:

    buildConfigField "float", "PI_VALUE", "3.14f"
    

    这会在BuildConfig中增加一个名为PI_VALUE的浮点数常量,其值为3.14。

  4. 长整数常量:

    buildConfigField "long", "MAX_SIZE", "100000000L"
    

    这会在BuildConfig中增加一个名为MAX_SIZE的长整数常量,其值为100000000。

  5. 字符常量:

    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抽离为多个外部文件,依照特征进行分批次保存,进一步进步代码的可读性。