您的位置:首页 > 移动开发 > Android开发

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

2016-08-19 11:54 696 查看

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

不得不说,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



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



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

通常我们的应用都会有开发环境(也可以理解为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";


  接下来要做的就是怎么执行不同的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


  在命令行里面执行这个命令就可以打出所有的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

以上是我的配置过程,如有问题欢迎留言,互相学习。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android gradle maven build
相关文章推荐