Gradle脚本的那些玩法

未来已来2018-09-06 09:36

作者:刘宏韬

使用productFlavors构建变种版本

通常productFlavors在多渠道打包中被提起,起因是国内有众多Android App分发平台,各平台在统计或资源方面有定制需求。但实际上productFlavors应该被理解为差异化打包,目的是为了将同一App构建为不同的版本。

举例场景:

同一App有两个版本,分别为officialspecial,版本之间有多项差异:

1.为了不同版本可以同时安装,包名需要不同;

2.各版本线上服务器地址不同;

3.某一个功能在offcial中启用,在special中不启用;

4.各版本桌面Launcher Icon不同;

  • 对于不同包名的需求,可以在各Flavor中配置各自对应的applicationId

  • 对于需求2、3,需通过buildConfigField为不同Flavor生成字符串常量作为对应服务器地址,以及布尔值常量作为功能开关。

    每个module都会自动生成一个BuildConfig类,其中默认将build.gradle中部分配置生成为了类中常量,包括包名、Flavor、版本名、版本号等。此外还可以通过buildConfigFieldbuild.gradle中生成常量。

    buildConfigField是Gradle构建插件中ProductFlavor类的一个方法,它接受三个字符串类型参数typenamevalue,并将在BuildConfig中生成代码<type> <name> = <value>;。因此当value值是字符串字面量时,需要加上双引号。

  • 对于需求4,需要src/路径下创建对应渠道名的文件夹,然后创建对应res/文件夹和资源

    src/文件夹下通常会生成main/文件夹,如果没有创建渠道名的文件夹及相关定制内容,则默认使用main/文件夹下内容。

    该场景中文件结构如下,official将使用main/下资源,special将使用special/下资源。


main
    └── res
        ├──...
        ...
special
  └── res
      ├── mipmap-mdpi
      │   └── ic_launcher.png
      ├── mipmap-hdpi
      │   └── ic_launcher.png
      ├── mipmap-xhdpi
      │   └── ic_launcher.png
      ├── mipmap-xxhdpi
      │   └── ic_launcher.png
      └── mipmap-xxxhdpi
          └── ic_launcher.png
另外,代码也可以用这种方式来做到不同Flavor中的差异,也是构建与main中相同的包和文件,放置差异的文件即可。但与资源不同的是,资源允许main和Flavor同时存在,并且优先使用对应Flavor的资源;而代码不允许main和Flavor同时存在,需要删掉对应main中的文件,不然会报代码重复错误。
  • 再搞复杂一点~为了开发方便,线上环境online、开发环境dev需要做区分,也就是说Flavor中除了版本维度之外,还存在一个环境维度存在着差异。

    Flavor相关类中还存在一个dimension属性可以用于标识该Flavor属于哪个维度的配置。在该举例场景中,版本维度VERSION有2个变种,环境维度ENVIRONMENT有2个变种,也就是将产生2*2=4个差异化版本。当前选择的版本维度信息也将生成到BuildConfig类中,如版本SpecialOnline

public static final String FLAVOR = "SpecialOnline";
public static final String FLAVOR_VERSION = "Special";
public static final String FLAVOR_ENVIRONMENT = "Online";
此外在Android Studio侧边有`BuildVariants`窗口,将各版本(包括`buildTypes`)生成并列出下拉菜单;`Gradle`窗口中也生成了对应的命令,例如`assembleSpecialOnlineRelease`将打出Flavor为`SpecialOnline`的Release包,而`assembleRelease`将打出所有版本的Release包。

以上完整的`productFlavors`配置如下:




//注册维度
flavorDimensions("VERSION", "ENVIRONMENT")
productFlavors {
    official {
        dimension 'VERSION'
        applicationId 'com.lht.demo.official'
        buildConfigField('String', 'ONLINE_SERVER', '"https://official.lht.com"')
        buildConfigField('boolean', 'WELCOME_TOGGLE', 'true')
    }
    special {
        dimension 'VERSION'
        applicationId 'com.lht.demo.special'
        buildConfigField('String', 'ONLINE_SERVER', '"https://special.lht.com"')
        buildConfigField('boolean', 'WELCOME_TOGGLE', 'false')
    }

    online {
        dimension "ENVIRONMENT"
         versionNameSuffix '-online'
    }
    dev {
        dimension "ENVIRONMENT"
         versionNameSuffix '-dev'
    }
}

自动化打包流程

举例场景:

使用assembleRelease命令打包后

1.将apk文件名追加版本号和版本名

2.复制到工程根目录apk文件夹下

3.使用易盾进行加壳和签名

4.上传到网易内测分发平台

  • gradle.build中增加这样一段代码,它的意思是对于应用的所有变种版本,在assemble命令执行完后对于每一个输出执行操作。output便是assemble后的输出。

android.applicationVariants.all { variant ->
    variant.assemble.doLast {
        variant.outputs.each { output ->
            def outputFile = output.outputFile;

            if (outputFile != null && outputFile.name.endsWith('.apk') && variant.buildType.name == 'release') {
                //当buildType为release时才执行
                ...
            }
        }
    }
}
  • 对于需求1、2,用以下方法获取版本号和版本名拼接成新文件名后,复制文件到apk文件夹,此外还可以拼接commit的hash、tag等等

Groovy语法规定,单引号引入的字符串直接输出, println('Hello $world!');即输出Hello $world!;而双引号中$符号后跟随的是字符串变量名,将在输出中替换成字符串字面量,world='lht';println("Hello $world!");即输出Hello lht!

def copyFileToApk(outputFile, variant) {
    def versionCode = variant.mergedFlavor.versionCode;
    def versionName = variant.mergedFlavor.versionName;
    def customName = '-' + versionName.replace('.', '-') + '-' + versionCode;

    def newName = outputFile.name.replace('.apk', customName + '.apk');

    copy {
        from "$outputFile"
        into "../apk"
        rename("$outputFile.name", "$newName")
    }

    return newName;
}
  • 对于需求3、4,将对应PROTECT_API_KEYPRODUCT_API_KEY放置在gradle.properties中,并在脚本中执行命令。工具命令根据版本不同而不同,具体命令格式参见各工具文档。以下示例代码具备相应前提,如已经生成签名文件、对应工具Jar包放置在apk文件夹下,使用时注意对应修改。

    使用易盾进行加壳和签名:

def apkProcess(apkFile) {
    def apkFilePath;
    //Window和Mac下路径分隔符不同
    if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
        apkFilePath = "apk\\$apkFile"
    } else {
        apkFilePath = "apk/$apkFile"
    }
        //工具命令根据版本不同而不同,具体命令格式参见各工具文档
    //执行加壳命令
    getRootProject().exec {
        executable 'java'
        args '-jar', 'apk/NHPProtect.jar', '-appkey', PROTECT_API_KEY, '-type', 'apkprotect', '-arg', '-antirepack', '-input', apkFilePath, '-output', apkFilePath
    }
    //执行签名命令
    getRootProject().exec {
        executable 'java'
        args '-jar', 'apk/apksigner.jar', '-notupdate', '-keystore', '【对应值】', '-alias', '【对应值】', '-pswd', SIGNING_PWD, '-aliaspswd', SIGNING_PWD, apkFilePath
    }
}

上传到网易内测分发平台

def apkUpload(apkFile, appName, typeVersion) {
    def apkFilePath = "apk/$apkFile"

    getRootProject().exec {
        executable 'curl'
        args '-v', '-F', 'authoremail=【对应值】',
                '-F', "productkey=$PRODUCT_API_KEY",
                '-F', "appname=$appName",
                '-F', 'appnotes=@note.txt',
                '-F', "app=@$apkFilePath",
                '-F', "typeversion=$typeVersion",
                'http://app.hz.netease.com/tasks/create'
    }
}
  • 此外在打包流程中其他步骤也可以集成到脚本中,充分利用脚本的优势,例如上传mapping文件到崩溃检测平台、将打包的各模块git tag整理成文档等

模块App化

在日常开发中调试某一功能,如果页面层级较深或App构建时间较长,会浪费很多开发精力。教育产品部各产品线的Android客户端采用了业务功能模块化的设计,可以在Gradle脚本中增加配置,使得模块可以独立成App运行,进而缩短开发时间。

  • 先定义一个布尔常量用于控制模块是否独立成App运行:final def AS_APP = false;
  • 将头部插件的导入增加一层判断,如要模块独立运行则使用com.android.application插件:

if (AS_APP) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
  • 要能独立运行,则应该有对应的ApplicationId,在defaultConfig中增加配置:

if (AS_APP) {
    applicationId "com.netease.edu.study"
}
  • 独立运行还需要对应的AndroidManifest和入口界面代码,这部分代码可以放在src/demo文件夹下,并在AS_APP开关打开时配置为sourceSets,这样当开关关闭时,代码不会被编译和打包

sourceSets {
    main {
        if (AS_APP) {
            java.srcDir 'src/demo/java'
            res.srcDir 'src/demo/res'
            manifest.srcFile 'src/demo/AndroidManifest.xml'
        }
    }
}

java.srcDirs ['src/java']写法是替换目录,java.srcDir 'src/java'是追加目录

  • 如果对于独立运行的App还有其他的需求,也可以使用该开关进行模式的切换,如独立运行时需要签名、独立运行时包的依赖需从provided改为compile

工程常量

工程常量指的是工程中所有模块共用的一组常量,例如当前是否是调试版本、是否开启HTTPS等。这些常量放在上层模块中,则下层模块无法访问;而底层模块有多个,并不能互相访问到。

可以将这些常量配置在gradle.properties中,并通过buildConfigField生成到BuildConfig类。

  • gradle.properties中配置如下常量:

# 工程常量
## 是否是调试版本
GLOBAL_DEBUG=false
## HTTPS 开关
GLOBAL_HTTPS=true
  • 工程根目录下新建global_config.gradle文件:

android{
    defaultConfig{
        buildConfigField("boolean", "GLOBAL_DEBUG", GLOBAL_DEBUG)
        buildConfigField("boolean", "GLOBAL_HTTPS", GLOBAL_HTTPS)
    }
}
  • 在需要使用这些常量模块的build.gradle文件头部将global_config.gradle导入:

apply plugin: 'com.android.library' // 需在这一行后
apply from: "$rootProject.projectDir/global_config.gradle"
  • 至此在BuildConfig中便生成了上述常量:

// Fields from default config.
public static final boolean GLOBAL_DEBUG = false;
public static final boolean GLOBAL_HTTPS = true;


网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者刘宏韬授权发布