美文网首页
道的 Gradle 高级技巧(一)

道的 Gradle 高级技巧(一)

作者: 程序员飞飞 | 来源:发表于2019-01-30 10:58 被阅读3次

    作为一名Android 开发者,我们都有发布 APP 内测版本的经历,有的公司是发布到自己的服务器上,生成一个链接或者二维码扫描就可以下载,有的公司使用一些内测平台如蒲公英、fir.im 等,有这么好的内测平台为什么不用呢?一方面这些平台的基本都是免费提供服务的,另外一方面也提供了许多丰富的 api,方便开发者使用,提升效率!

    1.

    前段时间研究 Android 端的自动打包,采用的是 Jenkins + Git 的方式,这样,当你 push 完代码之后,Jenkins 会自动拉取你的代码,然后再用 Gradle 工具进行自动化打包,Jenkins 可以配置许多插件,当打包完之后可以自动上传到蒲公英和 fir.im 等平台,特别方便,基本就是这么个流程,我们之前公司是运维帮我们在服务器端搭建的自动打包程序,这样的好处是当你需要打几十个包时,特别快,因为服务器的配置一般比电脑都高,并且不用占用自己电脑的资源。我自己前段时间也在自己的电脑上装了一个 Jenkins,然后一些配置,也可以进行自动化打包,但是我觉得没必要,因为你把代码 push 上去,然后再拉下来,然后再打包,用的还是你本地电脑的资源,还不如直接用 AS 打包来的快,我看了下 Jenkins 上传到公测平台的实现,其实就是用了一个 curl 命令来实现的,我就想着能不能在 Gradle 中配置上传的脚本?答案当然是可以的!

    2.

    首先,我们了解下什么是 curl?

    下面的概念来自某度的解释:

    curl 命令是一个利用 URL 规则在命令行下工作的文件传输工具。它支持文件的上传和下载,所以是综合传输工具,但按传统,习惯称 curl 为下载工具。作为一款强力工具,curl 支持包括 HTTP、HTTPS、ftp 等众多协议,还支持 POST、cookies、认证、从指定偏移处下载部分文件、用户代理字符串、限速、文件大小、进度条等特征。

    简单说他就是一个命令,Linux 和 Mac 系统自带,Windows 需要安装 curl 才能使用,在哪里下载 curl?为了方便大家我已经帮大家下载好了,复制下面的字符发送到后台即可:

    curl

    里面包含 32 位 & 64 位的安装程序,还有安装教程的链接供大家参考,安装完成后需要配置环境变量,然后在 cmd 中输入

    curl --version
    

    如果显示 curl 的版本号说明安装成功了~

    3.

    有了 curl 命令,我们就可以执行 curl 命令来进行上传文件了,我们先看下蒲公英上传 apk 的接口文档,如下图:

    pgyer_pic1.png

    其中,_api_key、uKey 和 file 字段是必须的,其他参数都是可选项,_api_key 和 uKey 蒲公英都会为每个 APP 自动分配一个,在你的蒲公英账号对应的 APP 的信息中可以找到,file 参数就是要上传的文件了,为了让我们上传的 APP 有每次更新的描述,我们还需要添加一个 buildUpdateDescription 字段,这样,每次上传 APP 之后就可以显示本次更新的描述信息了。

    文档看完之后,我们需要使用 curl 命令来发送一个上传文件的 POST 请求,url 的语法如下:

    curl(选项)(参数)
    
    curl_option.png

    curl 命令的选项有很多,上图只是截取了其中的一部分,根据蒲公英上传 APP 的接口文档,提交的是 multipart/form-data 类型的数据,因此我们使用 -F 选项,我们可以写出伪命令了:

    curl -F 参数1 -F 参数2 -F 参数3 接口地址
    

    因为我们上传 APP 需要 3 个参数,所以这里也需要三个参数,后面再加上我们请求的接口地址就可以了,这应该很好理解吧?

    我们再把参数部分替换成真实的的参数,完整的命令如下:

    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 的全路径,代码如下:

    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() 说一下,它可以获取你当前项目在你本地电脑的全路径。

    方法写好了,我们还需要把打包脚本稍微修改下,具体如下:

    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.提升用户体验!'
    }
    

    上面的代码,非常简单,一目了然,只要是程序员都能看得懂,如果你看不懂,说明你是伪程序员!

    好了,终于完成了,我们最终上传的命令被改造成了这样:

    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 中执行一条命令就可以实现打包上传了:

    ./gradlew uploadApk
    

    温馨提示:Windows 用户执行命令不需要加 ./

    怎么样?是不是很爽!这个效率上的提升不是一点半点,后续我在想,能不能把这个功能封装成一个 Gradle 插件的形式,提供给大家使用,这样也许只要添加一两行代码引用一下插件的就可以使用了,这样就更加方便了,敬请期待吧~

    如果我的文章对你有用,欢迎留言,点赞!

    本文首发于我的微信公众号,更多干货文章,请扫码订阅:


    IT大飞说

    相关文章

      网友评论

          本文标题:道的 Gradle 高级技巧(一)

          本文链接:https://www.haomeiwen.com/subject/trvqsqtx.html