一、自动构建背景
日常出版本和线上出版本时,需要手动修改一些配置,包括key配置、线上/测试环境配置、版本号增加等,过程繁琐。所以对构建脚本进行改进,达到自动构建目的。
PS:build.gradle 完整脚本在文章末尾
二、自动打包说明
1. 打测试包
命令:gradle assembleTest
2. 打线上包
命令:gradle assembleRelease
3. 根据需要修改
(1)版本号,前三个字段在build.gradle修改
版本号的第四个字段初始值在conf.properties文件修改:VERSION_NAME字段
该字段值每打包一次,自增1
(2)输出目录设置
在conf.properties文件修改: OUT_APK_PATH字段
4. debug时如果不希望版本号自增,有两种方式
app_version_value,直接改为版本号,如:1.1.1.2
<string name="app_version">app_version_value</string>
(2)在build.gradle脚本中,getVersionName函数里,在if判断条件中,把两个debug去掉
if(Consts.buildTypeOffline in runTasks || Consts.buildTypeDebug in runTasks || Consts.buildTypeRunDebug in runTasks) { //打测试包时才自增版本号, 线上包的版本号手动改
targetVerName += "." + (++verName); //版本号自增1
versionProps[Consts.verNamePropKey] = verName.toString();
versionProps.store(versionPropsFile.newWriter(), null) //增1后的版本号写入properties文件
}
三、脚本说明
1. key配置
老的方式:在Manifest文件中,直接写key值,如下:
<meta-data
android:name="YZ_APP_ID"
android:value="XXXXXXXXXXXXXX"/>
新的方式:通过变量的形势定义在Manifest中,build.gradle中根据不同的buildType,设置不同的值,如下,Manifest中定义${YZ_APP_ID}:
<meta-data
android:name="YZ_APP_ID"
android:value="${YZ_APP_ID}" />
在build.gradle中配置如下,打包时就会根据当前的buildType使用不同的值,比如打线上包 assembleRelease,则使用1处的值,打测试包assembleOffline,则使用2处的值
buildTypes {
release { //线上版本配置
//下面的值根据自己项目需要进行修改
manifestPlaceholders = [YZ_APP_ID:"xxxxxxxxx", YZ_APP_SECRET:"xxxxxxxxxxxxxxxx", RONG_CLOUD_APP_KEY:"xxxxxxxxxx"]
minifyEnabled true
zipAlignEnabled true //开启Zipalign优化
shrinkResources true //移除无用的resource文件,此项只有在开启混淆时才生效
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
signingConfig signingConfigs.release
}
offline { //测试版本配置
manifestPlaceholders = [YZ_APP_ID:"xxxxxxxxxxxxxx", YZ_APP_SECRET:"xxxxxxxxxxxxxxx", RONG_CLOUD_APP_KEY:"xxxxxxxxxxxxx"]
minifyEnabled true
zipAlignEnabled true //开启Zipalign优化
shrinkResources true //移除无用的resource文件,此项只有在开启混淆时才生效
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
signingConfig signingConfigs.release
}
debug {//开发版本配置
manifestPlaceholders = [YZ_APP_ID:"xxxxxxxxxxxxxx", YZ_APP_SECRET:"xxxxxxxxxxxxxxxxx", RONG_CLOUD_APP_KEY:"xxxxxxxxxxxxxxxxx"]
signingConfig signingConfigs.release
}
}
2. 线上/测试环境配置
环境在文件current_use.properties中配置,编译之前,需要把该文件内容替换为线上/测试的配置内容,在build.gradle用脚本实现该步骤,如下,这个比较简单,进行文件内容的拷贝:
我们的项目情况是这样的:线上/测试的环境配置,包括url等, 分别放test.properties在两个配置文件中写好:online.properties、test.properties,打包时,如果打线上包,则把online.properties文件内容复制到current_use.properties中,打测试包,则把test.properties文件内容复制到current_use.properties中,代码从current_use.properties中文件中读取;
/**
* current_use.properties内容设置,根据线上、线下区分
*/
def copyCurprop() {
def curPropFile = file(Consts.curUsePropFp);
def varPropFp;
def runTasks = gradle.startParameter.taskNames;
if(Consts.buildTypeRelease in runTasks) { //线上
varPropFp = Consts.onlinePropFp;
}else { //测试
varPropFp = Consts.offlinePropFp;
}
println "copyCurprop :" + varPropFp;
def varPropFile = file(varPropFp);
String varContent = varPropFile.getText(Consts.UTF8);
curPropFile.write(varContent, Consts.UTF8);
}
3. 版本号自增
我们的项目版本号规则有点复杂,如果你自己的项目没这么复杂,那么本文章你参考一下就可以了。
目前客户端测试包版本号为4个字段,最后一个字段每打一个版本,版本号增1;而线上版本版本号为前3个字段;用脚本实现测试包版本号自增1。
(1)在build.gradle文件中,手动设置好版本号的前3位(前3位版本号主观修改),如:1.1.1;
(2)计算出当前自增后的版本号
/** 版本号前3段: 根据需要修改 */
def verNamePrefix = "1.1.1"
/** 计算出当前版本号,自增 */
def currentVersionName = getVersionName(verNamePrefix)
计算当前版本号脚本如下,如果是打线上包,则版本号为手动设置的前3个字段,如果是测试包,则从配置文件中读取出第上一次的版本号(第四个字段),版本号加1,与前3个字段合并,组成新的版本号;
/**
* 版本号自增计算,测试环境版本号为4个段,线上环境版本号为3个段
* @param verNamePrefix : 版本号前缀
* @return
*/
def getVersionName(verNamePrefix) {
def targetVerName = verNamePrefix;
def versionPropsFile = file(Consts.confPropFp);
if(versionPropsFile.canRead()) {
println 'conf.properties can read'
def Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
def verName = versionProps[Consts.verNamePropKey].toInteger()
def runTasks = gradle.startParameter.taskNames
println 'in getVersionName , runTasks: ' + runTasks;
if(Consts.buildTypeOffline in runTasks || Consts.buildTypeDebug in runTasks || Consts.buildTypeRunDebug in runTasks) { //打测试包时才自增版本号, 线上包的版本号手动改
targetVerName += "." + (++verName); //版本号自增1
versionProps[Consts.verNamePropKey] = verName.toString();
versionProps.store(versionPropsFile.newWriter(), null) //增1后的版本号写入properties文件
}
}
println "targetVerName: " + targetVerName;
return targetVerName;
}
(3)编译过程中,对产生的中间文件values.xml的版本号进行修改,脚本如下:
/**
* 修改版本号, 修改gradle生成的中间文件values.xms
*/
def replaceVerName = { variant, fromString, toString ->
File valuesFile = file("${buildDir}/intermediates/res/merged/${variant.dirName}/values/values.xml");
println "in replaceVerName , toString: " + toString;
if (valuesFile.canRead()) {
println 'in replaceVerName, values.mxl can read'
String content = valuesFile.getText(Consts.UTF8);
content = content.replaceAll(fromString, toString);
valuesFile.write(content, Consts.UTF8);
}else {
println 'in replaceVerName, values.mxl can not read'
}
}
android.applicationVariants.all { variant ->
// apk输出路径,注意,从AS运行,默认使用debug包路径
def runTasks = gradle.startParameter.taskNames
if(Consts.buildTypeRelease in runTasks || Consts.buildTypeOffline in runTasks) {
variant.outputs.each{ output ->
output.outputFile = new File(getOutApkPath(variant, currentVersionName));
}
}
// 删除gradel生成的中间文件values.xml,确保执行mergeResources这个tasks; PS:debug时如果无需合并资源,则不会走mergeResources,所以通过删除values.xml,确保执行
if(Consts.buildTypeRunDebug in runTasks || Consts.buildTypeDebug in runTasks) {
deleteValueFile(variant);
}
// 版本号自增,在mergeResources task结束后执行
variant.mergeResources.doLast {
println "mergeResources.doLast, dir name : " + variant.dirName;
replaceVerName(variant, Consts.appVerNameValue, currentVersionName);
}
}
PS: 这里不直接修改strings.xml,因为如果本次打包修改了strings.xml,那么下次打包时strings.xml里无法通过匹配替换版本号;
debug运行时,如果资源文件没任何修改且之前有编译过了,则不会执行mergeResource这个task,所以debug打包时会先删除values.xml文件,确保执行mergeResource这个task,达到版本号自增的目的
4. apk输出目录配置
每次打包完后,需要把apk文件拷贝到指定目录,并按规则给apk命名: kydd_线上/测试环境_versionName.apk。目录在配置文件(conf.properties)中设置好,如下:
编译前,设置好输出路径:
android.applicationVariants.all { variant ->
// apk输出路径,注意,从AS运行,默认使用debug包路径
def runTasks = gradle.startParameter.taskNames
if(Consts.buildTypeRelease in runTasks || Consts.buildTypeOffline in runTasks) {
variant.outputs.each{ output ->
output.outputFile = new File(getOutApkPath(variant, currentVersionName));
}
}
输出路径拼接:
/**
* 输出apk文件完整路径,apk命名方式:根据项目需要自己定
* @param variant
* @param versionName
* @return
*/
def getOutApkPath(variant, versionName) {
def outPath = "./"
def confPropsFile = file(Consts.confPropFp);
if(confPropsFile.canRead()) {
def Properties confProps = new Properties();
confProps.load(new FileInputStream(confPropsFile));
outPath = confProps[Consts.outApkPathPropKey];
}
def String finalOutPath = outPath + "XXXX_v" + versionName + ".apk"; //apk名称自己定义
println "最终输出路径:" + finalOutPath
return finalOutPath;
}
主项目的build.gradle 完整内容:
apply plugin: 'com.android.application';
android {
/** 版本号前3段: 根据需要修改 */
def verNamePrefix = "1.1.1"
/** 计算出当前版本号,自增 */
def currentVersionName = getVersionName(verNamePrefix)
/** 根据buildType,拷贝当前的环境文件:线上 或者 测试 */
copyCurprop();
signingConfigs {
release {
keyAlias 'xxx'
keyPassword 'xxxxx'
storeFile file('xxxx.keystore')
storePassword 'xxxx'
v2SigningEnabled false
}
}
compileSdkVersion 24
buildToolsVersion '25.0.1'
aaptOptions.cruncherEnabled = false
aaptOptions.useNewCruncher = false
lintOptions {
abortOnError false
}
//android6.0 没有httpclient了,如用android23编译,需要加上该行,android23以下的不需要加这行
useLibrary 'org.apache.http.legacy'
defaultConfig {
applicationId "com.wtyt.yzone"
minSdkVersion 13
targetSdkVersion 23 //只能用23,否则融云SDK在android7上无法连接;如果要使用大于23,则需要加入 sqlite.so等包
versionName currentVersionName
//支持多个dex文件的编译
multiDexEnabled true
jackOptions {
enabled false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
buildTypes {
release { //线上版本配置
manifestPlaceholders = [YZ_APP_ID:"xxxx", YZ_APP_SECRET:"xxxxx", RONG_CLOUD_APP_KEY:"xxxxxx"]
minifyEnabled true
zipAlignEnabled true //开启Zipalign优化
shrinkResources true //移除无用的resource文件,此项只有在开启混淆时才生效
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
signingConfig signingConfigs.release
}
offline { //测试版本配置
manifestPlaceholders = [YZ_APP_ID:"xxxxxxx", YZ_APP_SECRET:"xxxxxxxxxxxxx", RONG_CLOUD_APP_KEY:"xxxxxxxxxxxx"]
minifyEnabled true
zipAlignEnabled true //开启Zipalign优化
shrinkResources true //移除无用的resource文件,此项只有在开启混淆时才生效
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
signingConfig signingConfigs.release
}
debug {//开发版本配置
manifestPlaceholders = [YZ_APP_ID:"xxxxxxxxxxxxxx", YZ_APP_SECRET:"xxxxxxxxxxxxxxxxx", RONG_CLOUD_APP_KEY:"xxxxxxxxxxxxxx"]
signingConfig signingConfigs.release
}
}
/**
* 修改版本号, 修改gradle生成的中间文件values.xms
*/
def replaceVerName = { variant, fromString, toString ->
File valuesFile = file("${buildDir}/intermediates/res/merged/${variant.dirName}/values/values.xml");
println "in replaceVerName , toString: " + toString;
if (valuesFile.canRead()) {
println 'in replaceVerName, values.mxl can read'
String content = valuesFile.getText(Consts.UTF8);
content = content.replaceAll(fromString, toString);
valuesFile.write(content, Consts.UTF8);
}else {
println 'in replaceVerName, values.mxl can not read'
}
}
/** 删除中间生成文件 values.xml */
def deleteValueFile = {variant ->
File valuesFile = file("${buildDir}/intermediates/res/merged/${variant.dirName}/values/values.xml");
try {
println '删除文件路径:' + valuesFile.absolutePath;
valuesFile.delete();
}catch (Exception e) {
println '删除文件异常:' + e.getMessage();
}
}
android.applicationVariants.all { variant ->
// apk输出路径,注意,从AS运行,默认使用debug包路径
def runTasks = gradle.startParameter.taskNames
if(Consts.buildTypeRelease in runTasks || Consts.buildTypeOffline in runTasks) {
variant.outputs.each{ output ->
output.outputFile = new File(getOutApkPath(variant, currentVersionName));
}
}
// 删除gradel生成的中间文件values.xml,确保执行mergeResources这个tasks; PS:debug时如果无需合并资源,则不会走mergeResources,所以通过删除values.xml,确保执行
if(Consts.buildTypeRunDebug in runTasks || Consts.buildTypeDebug in runTasks) {
deleteValueFile(variant);
}
// 版本号自增,在mergeResources task结束后执行
variant.mergeResources.doLast {
println "mergeResources.doLast, dir name : " + variant.dirName;
replaceVerName(variant, Consts.appVerNameValue, currentVersionName);
}
}
/*configurations {
compile.exclude group: "me.imid.swipebacklayout.lib.app", module: "swipeBackLib"
}*/
}
/**
* current_use.properties内容设置,根据线上、线下区分
*/
def copyCurprop() {
def curPropFile = file(Consts.curUsePropFp);
def varPropFp;
def runTasks = gradle.startParameter.taskNames;
if(Consts.buildTypeRelease in runTasks) { //线上
varPropFp = Consts.onlinePropFp;
}else { //测试
varPropFp = Consts.offlinePropFp;
}
println "copyCurprop :" + varPropFp;
def varPropFile = file(varPropFp);
String varContent = varPropFile.getText(Consts.UTF8);
curPropFile.write(varContent, Consts.UTF8);
}
/**
* 版本号自增计算,测试环境版本号为4个段,线上环境版本号为3个段
* @param verNamePrefix : 版本号前缀
* @return
*/
def getVersionName(verNamePrefix) {
def targetVerName = verNamePrefix;
def versionPropsFile = file(Consts.confPropFp);
if(versionPropsFile.canRead()) {
println 'conf.properties can read'
def Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
def verName = versionProps[Consts.verNamePropKey].toInteger()
def runTasks = gradle.startParameter.taskNames
println 'in getVersionName , runTasks: ' + runTasks;
if(Consts.buildTypeOffline in runTasks || Consts.buildTypeDebug in runTasks || Consts.buildTypeRunDebug in runTasks) { //打测试包时才自增版本号, 线上包的版本号手动改
targetVerName += "." + (++verName); //版本号自增1
versionProps[Consts.verNamePropKey] = verName.toString();
versionProps.store(versionPropsFile.newWriter(), null) //增1后的版本号写入properties文件
}
}
println "targetVerName: " + targetVerName;
return targetVerName;
}
/**
* 输出apk文件完整路径,apk命名方式(自己去定):xxxxxxxxx_versionName.apk
* @param variant
* @param versionName
* @return
*/
def getOutApkPath(variant, versionName) {
def outPath = "./"
def confPropsFile = file(Consts.confPropFp);
if(confPropsFile.canRead()) {
def Properties confProps = new Properties();
confProps.load(new FileInputStream(confPropsFile));
outPath = confProps[Consts.outApkPathPropKey];
}
def String finalOutPath = outPath + "xxxxxxx_v" + versionName + ".apk"; //apk名称自己去定
println "最终输出路径:" + finalOutPath
return finalOutPath;
}
/**
* 脚本用到的常量定义
*/
interface Consts{
/** buildType 类型名 */
final String buildTypeRelease = 'assembleRelease';
final String buildTypeOffline = 'assembleOffline';
final String buildTypeDebug = 'assembleDebug';
final String buildTypeRunDebug = ':app:assembleDebug'; //自己debug的时候看一下自己主工程名
/** current_use.properties 文件路径 */
final String curUsePropFp = './src/main/res/raw/current_use.properties';
/** online.properties 文件路径 */
final String onlinePropFp = './src/main/res/raw/online.properties';
/** test.properties 文件路径 */
final String offlinePropFp = './src/main/res/raw/test.properties';
final String UTF8 = 'UTF-8';
/** 配置文件名 */
final String confPropFp = 'conf.properties';
/** VERSION_NAME 在配置文件中的字段名:版本号中的第四个字段 */
final String verNamePropKey = 'VERSION_NAME';
/** OUT_APK_PATH 在配置文件中的字段名:apk输出目录 */
final String outApkPathPropKey = 'OUT_APK_PATH';
/** 在strings.xml中版本号值名称 */
final String appVerNameValue = 'app_version_value';
}
dependencies {
compile project(':xutils')
compile 'com.google.code.gson:gson:2.2.4'
compile 'com.android.support:appcompat-v7:21.0.3'
compile fileTree(include: '*.jar', exclude: 'android-support-multidex.jar', dir: 'libs')
compile 'com.android.support:multidex:1.0.1'
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.+'
}
项目中的conf.properties文件内容:
其中,字段值根据自己项目进行配置
#Thu Feb 16 12:47:40 CST 2017
OUT_APK_PATH=F\:/kydd/android_apk_out/
VERSION_NAME=14