productFlavors
构建变种版本
通常productFlavors
在多渠道打包中被提起,起因是国内有众多Android App分发平台,各平台在统计或资源方面有定制需求。但实际上productFlavors
应该被理解为差异化打包,目的是为了将同一App构建为不同的版本。
举例场景:
同一App有两个版本,分别为official、special,版本之间有多项差异:
1.为了不同版本可以同时安装,包名需要不同;
2.各版本线上服务器地址不同;
3.某一个功能在offcial中启用,在special中不启用;
4.各版本桌面Launcher Icon不同;
对于不同包名的需求,可以在各Flavor中配置各自对应的applicationId
。
对于需求2、3,需通过buildConfigField
为不同Flavor生成字符串常量作为对应服务器地址,以及布尔值常量作为功能开关。
每个module都会自动生成一个BuildConfig
类,其中默认将build.gradle
中部分配置生成为了类中常量,包括包名、Flavor、版本名、版本号等。此外还可以通过buildConfigField
在build.gradle
中生成常量。
buildConfigField
是Gradle构建插件中ProductFlavor
类的一个方法,它接受三个字符串类型参数type
、name
和value
,并将在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时才执行
...
}
}
}
}
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_KEY
和PRODUCT_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'
}
}
在日常开发中调试某一功能,如果页面层级较深或App构建时间较长,会浪费很多开发精力。教育产品部各产品线的Android客户端采用了业务功能模块化的设计,可以在Gradle脚本中增加配置,使得模块可以独立成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'
是追加目录
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
本文来自网易实践者社区,经作者刘宏韬授权发布