你不知道的 Gradle 高级技巧(一)
作为一名Android 开发者,我们都有发布 APP 内测版本的经历,有的公司是发布到自己的服务器上,生成一个连接或者二维码扫描就可以下载,有的公司使用一些内测平台如蒲公英、fir.im 等,有这么好的内测平台为什么不用呢?一方面这些平台的基本都是免费提供服务的,另外一方面也提供了许多丰富的 api,方便开发者使用,提升效率!
1.
前段时间研究 Android 端的自动打包,采用的是 Jenkins Git 的方式,这样,当你 push 完代码之后,Jenkins 会自动拉取你的代码,然后再用 Gradle 工具进行自动化打包,Jenkins 可以配置许多插件,当打包完之后可以自动上传到蒲公英和 fir.im 等平台,特别方便,基本就是这么个流程,我们之前公司是运维帮我们在服务器端搭建的自动打包程序,这样的好处是当你需要打几十个包时,特别快,因为服务器的配置一般比电脑都高,并且不用占用自己电脑的资源。我自己前段时间也在自己的电脑上装了一个 Jenkins,然后一些配置,也可以进行自动化打包,但是我觉得没必要,因为你把代码 push 上去,然后再拉下来,然后再打包,用的还是你本地电脑的资源,还不如直接用 AS 打包来的快,我看了下 Jenkins 上传到公测平台的实现,其实就是用了一个 curl
命令来实现的,我就想着能不能在 Gradle 中配置上传的脚本?答案当然是可以的!
2.
首先,我们了解下什么是 curl?
下面的概念来自某度的解释:
代码语言:javascript复制curl 命令是一个利用 URL 规则在命令行下工作的文件传输工具。它支持文件的上传和下载,所以是综合传输工具,但按传统,习惯称 curl 为下载工具。作为一款强力工具,curl 支持包括 HTTP、HTTPS、ftp 等众多协议,还支持 POST、cookies、认证、从指定偏移处下载部分文件、用户代理字符串、限速、文件大小、进度条等特征。
简单说他就是一个命令,Linux 和 Mac 系统自带,Windows 需要安装 curl 才能使用,在哪里下载 curl?为了方便大家我已经帮大家下载好了,复制下面的字符发送到后台即可:
**curl**
里面包含 32 位 & 64 位的安装程序,还有安装教程的链接供大家参考,安装完成后需要配置环境变量,然后在 cmd 中输入
代码语言:javascript复制curl --version
如果显示 curl 的版本号说明安装成功了~
3.
有了 curl 命令,我们就可以执行 curl 命令来进行上传文件了,我们先看下蒲公英上传 apk 的接口文档,如下图:
其中,_api_key、uKey 和 file 字段是必须的,其他参数都是可选项,_api_key 和 uKey 蒲公英都会为每个 APP 自动分配一个,在你的蒲公英账号对应的 APP 的信息中可以找到,file 参数就是要上传的文件了,为了让我们上传的 APP 有每次更新的描述,我们还需要添加一个 buildUpdateDescription 字段,这样,每次上传 APP 之后就可以显示本次更新的描述信息了。
文档看完之后,我们需要使用 curl 命令来发送一个上传文件的 POST 请求,url 的语法如下:
代码语言:javascript复制curl(选项)(参数)
curl 命令的选项有很多,上图只是截取了其中的一部分,根据蒲公英上传 APP 的接口文档,提交的是 multipart/form-data 类型的数据,因此我们使用 -F 选项,我们可以写出伪命令了:
代码语言:javascript复制curl -F 参数1 -F 参数2 -F 参数3 接口地址
因为我们上传 APP 需要 3 个参数,所以这里也需要三个参数,后面再加上我们请求的接口地址就可以了,这应该很好理解吧?
我们再把参数部分替换成真实的的参数,完整的命令如下:
代码语言:javascript复制curl -F "file=app/build/outputs/apk/release/release-v1.0.apk" -F "uKey=ce0e825125bfe666762b2a93feb7de00" -F "_api_key=534a49154990d8e9126918fbdbee600a" -F "buildUpdateDescription=bugs fix!" https://www.pgyer.com/apiv2/app/upload
好了,一条完整的 curl 上传命令算是完成了,其中,-F 后面的参数是字段名和参数的值,中间用 “=” 号进行连接,然后我们打开 AS 的 Terminal,执行上面的命令即可进行上传 apk 到蒲公英,上传过程也有进度显示,如果显示 100%,说明上传成功了,注意上面的 uKey 和 _api_key 的值换成你自己的,另外也要注意你打完包的 apk 文件路径要真实存在,否则会出现异常!
4.
上面我们已经学会了使用 curl 命令上传我们的 apk 了,但是你们有没有发现,我们每次上传 apk 需要好几步:
- 打包 apk;
- 修改上传命令中 apk 的文件名和描述信息;
- 复制 curl 命令到 Terminal 中执行;
- …
至少需要 3 步才能完成,这也是一件非常麻烦的事情,作为程序员,都是比较懒的,与其说懒不如说是机智,避免做浪费时间的事情,我再想,能不能一条命令一步到位呢?当然是可以的。
我们先进行改造上传命令的第一步,先把 curl 命令中 file 的值,也就是 apk 路径动态进行获取,这样就不用每次都去修改了,我们知道 gradle 语法中的字符串有两种,一种是单引号,另一种是双引号,区别就是,双引号支持插值,这样我们就可以写一个方法,这个方法的作用就是获取打包好的 apk 的全路径,代码如下:
代码语言:javascript复制def getApkFullPath() {
return rootDir.getAbsolutePath() "/app/build/outputs/apk/release/" getApkName()
}
def getApkName() {
return "update-app-example-v${android.defaultConfig.versionName}-${releaseTime()}.apk"
}
static def releaseTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
上面总共有 3 个方法,也比较简单,大家应该都能看得懂,就不过多解释了,其中第一个方法中的 rootDir.getAbsolutePath()
说一下,它可以获取你当前项目在你本地电脑的全路径。
方法写好了,我们还需要把打包脚本稍微修改下,具体如下:
代码语言:javascript复制release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = getApkName()
}
}
}
注意看上面的 outputFileName = getApkName()
,这样写每次生成的 apk 的名字就是我们要获取的 apk 的文件名,这样每次打包完后我们都能获取到打包完后的 apk 的文件名了!这样,我们 curl 上传命令中动态获取 apk 文件路径这个问题就算解决了,我们再看下我们的 curl 命令中还有 uKey 和 _api_key 两个参数,因为这两个值属于比较私密的东西我们一般都是配置到 local.properties
文件中,然后动态读取的,git 默认是忽略提交 local.properties
文件的,这样防止自己的私密信息被提交和泄露出去,因此,这里也写个方法动态读取一下吧:
def readProperties(key) {
File file = rootProject.file('local.properties')
if (file.exists()) {
InputStream inputStream = rootProject.file('local.properties').newDataInputStream()
Properties properties = new Properties()
properties.load(inputStream)
if (properties.containsKey(key)) {
return properties.getProperty(key)
}
}
}
这个方法也比较简单,相信大家都能看的明白!
现在我们的上传命令中还有一个字段 buildUpdateDescription
,更新描述信息,每次上传 apk 都需要修改一下更新的描述,直接在命令中修改,也不太好,因此我们也写个方法动态获取吧,如下所示:
static def getUpdateDescription() {
return '1.修复一些bug;n2.提升用户体验!'
}
上面的代码,非常简单,一目了然,只要是程序员都能看得懂,如果你看不懂,说明你是伪程序员!
好了,终于完成了,我们最终上传的命令被改造成了这样:
代码语言:javascript复制curl -F "file=@${getApkFullPath()}" -F "uKey=${readProperties('pgyer.userKey')}" -F "_api_key=${readProperties('pgyer.apiKey')}" -F "buildUpdateDescription=${getUpdateDescription()}" https://www.pgyer.com/apiv2/app/upload
5.
上面的命令算是改造完成了,不知道你们有没有发现,有个致命的问题就是,这条命令怎么执行啊?因为我么你的命令中动态调用了 Gradle 中我们写的方法,直接在 Terminal 中执行肯定是会报错的!这可就尴尬了。。我们想了想,要想让我们上传命令中的方法能够被成功调用,这个命令和被调用的方法肯定是在同一个 Gradle 文件中的,我们再想能不能写一个 Task,这这个 Task 中执行我们的上传命令,这样不就解决问题了吗?嗯,想了想是可以的,我发现在写的过程中 Task 好写,但是我们这个命令怎么才能够执行呢?肯定也需要一个东西才能执行我们的命令,搜了下,Gradle 中有个 exec
东西,它可以执行一条具体的 bash 命令,嗯,灰常不错,可以的,very good!真香!最终我们写的完整的 Task 如下:
task("uploadApk") {
def command = "curl -F "file=@${getApkFullPath()}" -F "uKey=${readProperties('pgyer.userKey')}" -F "_api_key=${readProperties('pgyer.apiKey')}" -F "buildUpdateDescription=${getUpdateDescription()}" https://www.pgyer.com/apiv2/app/upload"
println "command:" command
try {
exec {
ExecSpec execSpec ->
executable 'bash'
args '-c', command
}
println "uploadApk success~"
} catch (Exception e) {
e.printStackTrace()
}
}
上面的代码相对也比较简单,其中 ExecSpec
大家可能看着比较陌生,executable ‘bash’ 为固定写法,其中 bash 代表 shell 的类型,Linux 下有很多种 shell 的类型,流行的 shell 有 ash、bash、ksh、csh、zsh 等,一般我们常用的都是 bash,其中的 command 就是一条具体的命令了。
这样,我们只要执行这个 Task 就可以自动执行上传命令并动态获取我们所需要的参数了~
6.
上面的命令执行起来是非常方便的,但是在实际使用中,我们发现,需要先打完包之后才能执行上传的 Task,我们知道 Gradle 中的 Task 是可以依赖另一个 Task 的,打包命令实质上也是一个 Task,这样我们让我们自己写的 Task 依赖于打包的 Task 不就行了吗?我觉得没毛病,这样,当执行完打包后,自动执行上传命令,这样一条命令就可以解决问题,另外,我们自己的 Task 也需要稍微修改下,将上传的实现部分放到 doLast
闭包中,完成代码入下:
task("uploadApk") {
doLast {
def command = "curl -F "file=@${getApkFullPath()}" -F "uKey=${readProperties('pgyer.userKey')}" -F "_api_key=${readProperties('pgyer.apiKey')}" -F "buildUpdateDescription=${getUpdateDescription()}" https://www.pgyer.com/apiv2/app/upload"
println "command:" command
try {
exec {
ExecSpec execSpec ->
executable 'bash'
args '-c', command
}
println "uploadApk success~"
} catch (Exception e) {
e.printStackTrace()
}
}
}
uploadApk.dependsOn("assembleRelease")
从此,我们只要在 Terminal 中执行一条命令就可以实现打包上传了:
代码语言:javascript复制./gradlew uploadApk
温馨提示:Windows 用户执行命令不需要加 ./
怎么样?是不是很爽!这个效率上的提升不是一点半点,后续我再想能不能把这个功能封装成一个 Gradle 插件的形式,提供给大家使用,这样也许是要添加一两行代码引用一下插件的就可以使用了,这样就更加方便了,敬请期待吧~