Android使用Gradle命令动态传参完成打包不需要修改代码

来源:互联网 发布:用js给input赋值 编辑:程序博客网 时间:2024/05/16 02:11

Android使用Gradle命令动态传参完成打包,不需要修改代码

源代码: http://blog.csdn.net/u014637428/article/details/52249423?locationNum=14

不得不说,Gradle很强大,有人会问Gradle是什么?这里也不细讲,在我认为他就是一个构建神器。Gradle 提供了:

  • 一个像 Ant 一样的非常灵活的通用构建工具
  • 一种可切换的, 像 Maven 一样的基于合约构建的框架
  • 支持强大的多工程构建
  • 支持强大的依赖管理(基于 ApacheIvy )
  • 支持已有的 Maven 和 ivy 仓库
  • 支持传递性依赖管理, 而不需要远程仓库或者 pom.xml 或者 ivy 配置文件
  • 基于 Groovy 的构建脚本
  • 有丰富的领域模型来描述你的构建

build.gradle文件

  首先先来说说这个文件,大家都知道Android 项目默认目录下就有两个build.gradle文件,其实也类似Maven中的pom.xml文件,一个是Project范围的,另一个是Module范围的,由于一个Project可以有多个Module,所以每个Module下都会对应一个build.gradle。
  这两个文件是有区别的,Project下的build.gradle是基于整个Project的配置,而Module下的build.gradle是每个模块自己的配置。这里我主要讲一下module下的build.gradle文件,也就是通常说的默认Module app下的。
  讲到这个配置,还需要引入Gradle中的Task概念。Gradle的Project从本质上说只是含有多个Task的容器,一个Task与Ant的Target相似,表示一个逻辑上的执行单元。我们可以通过很多种方式定义Task,所有的Task都存放在Project的TaskContainer中,Task是Gradle的第一公民。
  Task可以自定义,但如果没有什么太大需求,其实几乎都用不到,因为Gradle会根据你的build.gradle自动创建Task,你只需要在配置文件里面配置一些需要的就可以了,在构建的时候会自动生成Task,用Android Studio的点击同步也会自动生成。
  可以打开Android Studio右侧的Gradle面板,双击就可以执行Task

android Gradle Task结构

  命令行里面可以直接使用gradle taskName(例如: gradle assemble360会将360市场的所有包都打出来,包括debug,release等,当然,这些还得你先在build.gradle里面配置好)。Android Studio 中也可以打开左下角的terminal中使用gradlew 执行命令,gradlew是gradle wrapper的简写,和gradle命令功能一样。

android studio terminal

  好,开始切入正题,假如现在有这样的需求:

  • 通常我们的应用都会有开发环境(也可以理解为debug环境)、测试环境、预发环境、正式环境区分,我想要不改代码就可以打出我想要环境的包。比如我现在分别想要一个测试环境的包和一个线上环境的包,但是我又不想改代码
  • 发布的时候发现版本号和版本名忘记改了
  • 我想要随时指定一个目录,将打包好的文件放在这里面
  • 我想要在打包时可以自定义安装包的文件名

  很简单,如果你不想改代码又想要得到不同环境的包,那当然是使用Gradle的命令,前面说过Gradle命令后面可以加上Task的name直接执行Task,那我们可以自己定义我们需要的Task,让不同的Task去做我们想要做的事不就解决问题了吗。
  可是下面又说要动态指定版本号版本名文件名和文件输出路径,那怎么办?
  也不难,传参,需要什么就传入什么,这样就解决了动态指定的问题了。

  思路讲到这里,我们来看看具体要怎么配置这个文件:

第一个问题:怎么去配置不同环境的Task?

  原先网络请求路径可能很多人都会写在代码里面,如下图所示

/** * 存放一些全局常量 */public class Constants {    //外网测试环境    public static final String BASEHTTP = "http://test.api.cn";    //线上地址//    public static final String BASEHTTP = "http://release.api.cn";    //预发环境//    public static final String BASEHTTP = "http://pre.api.cn";    //本地测试//    public static final String BASEHTTP = "http://dev.api.cn";    //登录    public static final String LOGIN_URL = BASEHTTP + "/api/user/login";}

  在需要更换环境的时候就换一个BASEHTTP的值,这样可以解决问题,但是每一次编译打包都需要重新去改一下代码。一两个包还好,如果多了就会觉得很麻烦,不方便。
  所以就想到了可不可以将这些信息都写在build.gradle配置文件里面,这样好像就可以跟Gradle有点挂钩了

    //正式环境    def API_RELEASE_HOST = "\"http://release.api.cn\""    //预发环境    def API_PRE_RELEASE_HOST = "\"http://pre.api.cn\""    //测试环境    def API_TEST_HOST = "\"http://test.api.cn\""    //开发环境    def API_DEV_HOST = "\"http://dev.api.cn\""

  Gradle脚本是用Groovy语言来写的,Groovy语言这里不细讲,大家可以网上搜Groovy语法,资料还是蛮多的,使用Groovy可以感受到到以下两个特点:

  • Groovy继承了Java的所有东西,就是你突然忘了Groovy的语法可以写成Java代码,也就是Groovy和Java混在一起也能执行。
  • Groovy和Java一样运行在JVM,源码都是先编译为class字节码。

  这里我用def定义了几个常量,分别用来表示不同的环境的请求地址,然后在defaultConfig里面自定义了一个常量名,作为代码与配置文件的桥梁,建立了连接。注意:这里的字符串需要在里面加入引号,用转义符转义,因为Groovy会直接把最外层引号内的值赋值给生成的自定义变量,如果不加,赋值后的String字符串就会没有引号,导致编译出错。

    defaultConfig {        applicationId "com.test"        minSdkVersion 15        targetSdkVersion 23        versionCode 5        versionName 1.1.0        buildConfigField("String", "API_HOST", "${API_DEV_HOST}")    }

  这里的buildConfigField就是自定义一个常量,第一个参数表示类型,第二参数表示常量名,第三个参数传入的是值。
  点击同步后在代码中就可以直接调用BuildConfig.API_HOST来使用了,因为当点击同步后,Gradle就会在BuildConfig这个类中加入常量API_HOST

    public final class BuildConfig {          public static final boolean DEBUG = Boolean.parseBoolean("true");          public static final String APPLICATION_ID = "com.test";          public static final String BUILD_TYPE = "debug";          public static final String FLAVOR = "360";          public static final int VERSION_CODE = 6;          public static final String VERSION_NAME = "1.1.1";          // Fields from default config.          public static final String API_HOST = "http://test.api.cn";    }

  可以看到BuildConfig这个类中的最后一行已经有了API_HOST这个常量了,还有一些其他的常量也是根据配置自动生成的,这里可以先不用管。
  现在可以通过代码请求到配置文件里面的配置了。

    public static final String LOGIN_URL = BuildConfig.API_HOST + "/api/user/login";
  • 1  接下来要做的就是怎么执行不同的task就会引用不同的配置。

  build.gradle文件中有一个buildTypes,里面放的是你在build的时候需要选择的类型,默认有一个debug,也可以自己自定义,我在这里加了四种类型,debug(开发)、beta(测试)、preRelease(预发)、release(正式发布)

    buildTypes {        /* 线上环境 */        release {            // 不显示Log            buildConfigField "boolean", "LOG_DEBUG", "false"            buildConfigField "String", "API_HOST", "${API_RELEASE_HOST}"//API Host            minifyEnabled true //是否混淆            //是否设置zip对齐优化            zipAlignEnabled true            // 移除无用的resource文件            shrinkResources true            //签名            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }        /* 预发环境 */        preRelease {            // 不显示Log            buildConfigField "boolean", "LOG_DEBUG", "false"            buildConfigField "String", "API_HOST", "${API_PRE_RELEASE_HOST}"//API Host            minifyEnabled true //是否混淆            //是否设置zip对齐优化            zipAlignEnabled true            // 移除无用的resource文件            shrinkResources true            //签名            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }        /* 本地开发环境 */        debug {            minifyEnabled false        }        /* 测试环境 */        beta {            // 显示Log            buildConfigField "boolean", "LOG_DEBUG", "true"            buildConfigField "String", "API_HOST", "${API_TEST_HOST}"//API Host            minifyEnabled true //是否混淆            //是否设置zip对齐优化            zipAlignEnabled true            // 移除无用的resource文件            shrinkResources true            //签名            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }

  可以看到,在每个buildType里面都有相应的配置,你打哪个类型的包,就会去读取哪个类型的配置,如果没有,默认会去读取defaultConfig里面的配置,defaultConfig里面相当于初始值,这样就做到了不同环境有了不同的配置,同步一下,再看一下Android Studio右侧的Gradle面板,可以发现多了一些Task,自动生成了一些Task,比如原先是assembleDebug,现在就多了assembleBeta、assemblePreRelease、assembleRelease,想要执行哪个环境就执行哪个任务就ok了。
  但是很多第三方的外部包配置不止在build.gradle文件,还会在AndroidManifest.xml做一些正式环境和测试环境的区分,做一些不同的配置,这里的配置怎么处理?

第二个问题:怎么将AndroidManifest.xml里面的配置在build.gradle里面进行配置?

  举个例子,我这里拿talkingData的配置来说,需要在AndroidManifest.xml里面指定APP_ID

    <!--TalkingData 配置-->    <meta-data        android:name="TD_APP_ID"        android:value="7E5389EAD0C2324FB7B379701F6D2BA0" />

  包括百度地图、个推等其实很多第三方库都需要配置这些,在AndroidManifest.xml里面可以直接引用build.gradle文件里面的配置,build.gradle里面怎么配置我们一会再讲,先看看引用配置后代码:

    <!--TalkingData 配置-->    <meta-data        android:name="TD_APP_ID"        android:value="${TALKING_DATA_APP_ID}" />

这里使用了引用了build.gradle里面的TALKING_DATA_APP_ID的值,我们再来看看build.gradle文件里面怎么配置。

    def TEST_TALKING_DATA_APP_ID = "6E5389EAD0C2C2CFB7B379701F6D2BA8"    defaultConfig {        applicationId "com.test"        minSdkVersion 15        targetSdkVersion 23        versionCode 5        versionName 1.1.0        buildConfigField("String", "API_HOST", "${API_DEV_HOST}")        manifestPlaceholders = [                /* talkingData 测试环境 */                TALKING_DATA_APP_ID: "${TEST_TALKING_DATA_APP_ID}"        ]    }

  我在defaultConfig里面指定了一个manifestPlaceholders属性,也是gradle默认就提供的一个属性,从形式可以看出是一个数组的形式,里面可以写多个键值对,用逗号隔开,AndroidManifest.xml会从manifestPlaceholders数组里面去寻找匹配的键,找到了就会引用这个键所对应的值。
  这样问题就迎刃而解了,所有的AndroidManifest.xml里面的配置都可以写在build.gradle里面统一处理了。
  而上面说过,defaultConfig是默认的配置,不同的buildType可以指定不同的配置,所以在不同的buildType,也可以理解为不同的环境里面配置不同的manifestPlaceholders就可以了。代码如下:

    buildTypes {        /* 线上环境 */        release {            // 不显示Log            buildConfigField "boolean", "LOG_DEBUG", "false"            buildConfigField "String", "API_HOST", "${API_RELEASE_HOST}"//API Host            manifestPlaceholders = [                    /*  release 环境  */                    GETUI_APP_ID       : "${RELEASE_GETUI_APP_ID}",                    GETUI_APP_KEY      : "${RELEASE_GETUI_APP_KEY}",                    GETUI_APP_SECRET   : "${RELEASE_GETUI_APP_SECRET}",                    /* talkingData release 环境 */                    TALKING_DATA_APP_ID: "${RELEASE_TALKING_DATA_APP_ID}",                    PACKAGE_NAME       : defaultConfig.applicationId            ]            minifyEnabled true //是否混淆            //是否设置zip对齐优化            zipAlignEnabled true            // 移除无用的resource文件            shrinkResources true            //签名            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }        /* 预发环境 */        preRelease {            // 不显示Log            buildConfigField "boolean", "LOG_DEBUG", "false"            buildConfigField "String", "API_HOST", "${API_PRE_RELEASE_HOST}"//API Host            manifestPlaceholders = [                    /*  release 环境  */                    GETUI_APP_ID       : "${RELEASE_GETUI_APP_ID}",                    GETUI_APP_KEY      : "${RELEASE_GETUI_APP_KEY}",                    GETUI_APP_SECRET   : "${RELEASE_GETUI_APP_SECRET}",                    /* talkingData release 环境 */                    TALKING_DATA_APP_ID: "${RELEASE_TALKING_DATA_APP_ID}",                    PACKAGE_NAME       : defaultConfig.applicationId            ]            minifyEnabled true //是否混淆            //是否设置zip对齐优化            zipAlignEnabled true            // 移除无用的resource文件            shrinkResources true            //签名            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }        /* 本地开发环境 */        debug {            minifyEnabled false        }        /* 测试环境 */        beta {            // 显示Log            buildConfigField "boolean", "LOG_DEBUG", "true"            buildConfigField "String", "API_HOST", "${API_TEST_HOST}"//API Host            manifestPlaceholders = [                    /*  个推测试环境   */                    GETUI_APP_ID       : "${TEST_GETUI_APP_ID}",                    GETUI_APP_KEY      : "${TEST_GETUI_APP_KEY}",                    GETUI_APP_SECRET   : "${TEST_GETUI_APP_SECRET}",                    /* talkingData 测试环境 */                    TALKING_DATA_APP_ID: "${TEST_TALKING_DATA_APP_ID}",                    PACKAGE_NAME       : defaultConfig.applicationId            ]            minifyEnabled true //是否混淆            //是否设置zip对齐优化            zipAlignEnabled true            // 移除无用的resource文件            shrinkResources true            //签名            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }

  我们已经可以执行不同的Task去打不同环境的包了。
  命令行里面可以使用gradle assembleBeta(assembleBeta表示测试环境,其他环境可以替换后面的名字,如assemblePreRelease、assembleRelease等,如果配置了渠道,还会在assemble后面拼上渠道名,例如我的渠道名是360,我要打release的包,那就是assemble360Release)。
  ide里面可以点击菜单上的build,再点击Generate Signed APK…,填完keystore密码后选择buildType进行打包操作。
  提醒:在打包前最好先做一下clean操作,否则会出现有些代码打包不进去,不知道其他人是不是这样的。

第三个问题:怎么动态传参满足需求?

很简单,直接上代码:

    defaultConfig {        applicationId "com.ixwork"        minSdkVersion 15        targetSdkVersion 23        //关键看这两行        versionCode project.hasProperty('VERSION_CODE') ? Integer.parseInt(VERSION_CODE) : DEF_VERSION_CODE        versionName project.hasProperty('VERSION_NAME') ? VERSION_NAME : "${DEF_VERSION_NAME}"        buildConfigField("String", "API_HOST", "${API_DEV_HOST}")    }
  •   关键看versionCode 和versionName这两行,原先默认是直接在后边写上版本号和版本名,这里用了三目运算符,可以用project.hasProperty(‘KEY’)来判断是否有KEY这个参数传入,如果有的话就就返回true,就会使用传入的值作为实际值,这里用了强转,将传入的String类型转为int类型的,如果没有就会返回false,使用默认的值。

  同理,传入文件名和文件输出路径也一样。

    //修改生成的最终文件名    applicationVariants.all { variant ->        variant.outputs.each { output ->            def outputFile = output.outputFile            if (outputFile != null && outputFile.name.endsWith('.apk')) {                //判断是否有这个OUT_PUT_DIR参数传入                File outputDirectory = new File(project.hasProperty('OUT_PUT_DIR') ? OUT_PUT_DIR : outputFile.parent);                def fileName                if(!project.hasProperty('FILE_NAME')){                    if (variant.buildType.name == "release" || variant.buildType.name == "preRelease") {                        // 输出apk名称为app_v1.0.0_2015-06-15_playStore.apk                        fileName = "app_v${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}_${variant.buildType.name}.apk"                    } else if (variant.buildType.name == "beta") {                        fileName = "app_v${defaultConfig.versionName}_${releaseTime()}_${variant.buildType.name}.apk"                    } else {                        fileName = outputFile.name                    }                }else{                    fileName = FILE_NAME                }//                println("输出apk ---> " + outputDirectory.absolutePath + File.separator + outputFile.name)                output.outputFile = new File(outputDirectory, fileName)            }        }    }

  因为我这里配置了多个渠道,所以使用variant.outputs.each循环输出文件,在里面分别处理每一个包。
  在代码里面分别用了project.hasProperty(‘OUT_PUT_DIR’)和project.hasProperty(‘FILE_NAME’)来判断是否有这个参数,有无参数分别做了不同的处理。
  fileName = “app_v{defaultConfig.versionName}_{releaseTime()}_{variant.productFlavors[0].name}_{variant.buildType.name}.apk”
这里对文件名进行了比较人性化的处理,加上了各种信息,通过包名就可以看出一些基本信息,如果不指定名字传入,就会使用这个默认的名字。

  万事俱备,只欠东风了。到这里,基本所有都说完了,最后还有一个问题,哈哈,如何传参?

第四个问题:如何传参?

    gradle clean assembleBeta -PVERSION_CODE=5 -PVERSION_NAME=1.1.1 -POUT_PUT_DIR=/home/user/Desktop -PFILE_NAME=test.apk
  • 1在命令行里面执行这个命令就可以打出所有的Beta包了(前提是已经安装好Gradle,并配置好Gradle的环境变量,或者使用IDE里面的terminal,在项项目目录下使用gradlew命令),其中assembleBeta 可以根据自己需求替换成其他的task名字。

  传参就是在后面加上 -P参数,-P后面再加上要传入的键值对,中间用=号连接,需要什么参数就传什么参数,如果有其他需要也可以自定义加入。

最后附上build.gradle源文件,可以点击此处下载 build.gradle

另外还有一篇关于Android使用Gradle配合Jenkins自动构建打包的文章,有兴趣的可以去看看。
http://blog.csdn.net/u014637428/article/details/52248589

以上是我的配置过程,如有问题欢迎留言,互相学习。

阅读全文
0 0
原创粉丝点击