欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-24 18:59:46
...

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命令动态传参完成打包不需要修改代码

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

Android使用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

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