Android使用Gradle命令动态传参完成打包不需要修改代码
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
命令行里面可以直接使用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";
- 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
以上是我的配置过程,如有问题欢迎留言,互相学习。