一、自动构建背景

   日常出版本和线上出版本时,需要手动修改一些配置,包括key配置、线上/测试环境配置、版本号增加等,过程繁琐。所以对构建脚本进行改进,达到自动构建目的。

   PS:build.gradle 完整脚本在文章末尾

 

android gradle task 传值 android gradle exclude_构建


二、自动打包说明

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