使用gradle多渠道打包也不是什么新鲜事了,配置productFlavors就完事了,再写点buildConfigField什么的,似乎也就可以用了。
用确实是可以用,但遇上某天想改打包出来的名字就很尴尬了,不知道怎么改。
使用
本来在build.gradle有这样的配置:
android {
buildTypes {
debug {
//省略
}
release {
//省略
}
}
productFlavors {
jinxin_bcca {
manifestPlaceholders = [CHANNEL_VALUE: "bcca"]
buildConfigField "String", "BCCA_HOST", '"222.82.20.98"'
buildConfigField "int","BCCA_PORT",'9529'
buildConfigField "int","DEVICE_TYPE",'0x00000631'
}
jinxin_bcca_debug {
manifestPlaceholders = [CHANNEL_VALUE: "bcca_debug"]
buildConfigField "String", "BCCA_HOST", '"39.10.93.118"'
buildConfigField "int","BCCA_PORT",'9529'
buildConfigField "int","DEVICE_TYPE",'0x00000601'
}
}
}
然后加一段配置apk文件名的代码:
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.buildType.name.equals('release')) {
outputFileName = "BeoneGateway3_V${defaultConfig.versionName}_${variant.productFlavors[0].getName()}.apk"
} else if (variant.buildType.name.equals('debug')) {
outputFileName = "BeoneGateway3_V${defaultConfig.versionName}_debug.apk"
}
}
}
这样使用了很长一段时间,这样打包出来的apk文件会被这段代码重新命名。一直以来,虽然不知道这段代码的具体语法,但是大概能揣摩每个变量的意思…也仅限于此。
摸索
后面稍微了解一下groovy语言,发现和java相当类似,那么又重新从语言的角度来看之前这段配置代码,于是改成了这样:
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.buildType.name.equals('release')) {
outputFileName = "SmartStreamGateway_V${defaultConfig.versionName}_${variant.getFlavorName()}.apk"
} else if (variant.buildType.name.equals('debug')) {
outputFileName = "SmartStreamGateway_V${defaultConfig.versionName}_${variant.getFlavorName()}_debug.apk"
}
}
}
并且也知道其中的variant其实就当于fori循环中的i一样,相当于一个形参,而{…}这一段就是一个闭包。更重要的是,gradle本身就是一个框架,其中也包含着各种类各种方法,这里的applicationVariants.all来自于定义:
public interface DomainObjectCollection<T> extends Collection<T> {
void all(Action<? super T> action);
void all(Closure action);
}
这里出现的variant:
public interface ApplicationVariant extends ApkVariant, TestedVariant {}
这些都是实打实的Java接口。
“Groovy编译器产生的字节码和Java编译器产生的字节码是完全一样的。这就等于说,Groovy能够完全使用各种Java API。” ----《Groovy入门经典》
有了这种认知,改配置代码其实就和改Java代码一样了。
ApplicationVariant常用到的一些方法都定义在父类*N的接口BaseVariant中:
public interface BaseVariant {
@NonNull
String getName();
@NonNull
String getDescription();
@NonNull
String getDirName();
@NonNull
String getFlavorName();
@NonNull
BuildType getBuildType();
@NonNull
ProductFlavor getMergedFlavor();
@NonNull
List<ProductFlavor> getProductFlavors();
@NonNull
List<SourceProvider> getSourceSets();
@NonNull
String getApplicationId();
void buildConfigField(@NonNull String type, @NonNull String name, @NonNull String value);
void resValue(@NonNull String type, @NonNull String name, @NonNull String value);
}
那么问题来了,现在我想以自己定义的一些值作为文件名,而且不能体现在ProductFlavor的名称中,因为这些值类似于"192.168.0.1",而这种命名不能用在这里(因为分隔符‘.'的原因)。
在上面的方法中有个叫getDescription的函数,本来以为在实现类中应该会有相应的setDescription方法,但事实证明想多了…
通过源码一路查找BaseVariantImpl–>BaseVariantData–>AndroidArtifactVariantData–>InstallableVariantData–>ApkVariantData:
public abstract class ApkVariantData extends InstallableVariantData {
@Override
@NonNull
public String getDescription() {
final GradleVariantConfiguration config = getVariantConfiguration();
if (config.hasFlavors()) {
StringBuilder sb = new StringBuilder(50);
StringHelper.appendCapitalized(sb, config.getBuildType().getName());
StringHelper.appendCapitalized(sb, config.getFlavorName());
sb.append(" build");
return sb.toString();
} else {
return StringHelper.capitalizeWithSuffix(config.getBuildType().getName(), " build");
}
}
}
发现这个方法是返回的“固定值”,类似于“buildType FlavorName build"这样的形式。于是死心。
还是得找找ProductFlavor类里有没有可以用来作为命名变量的东西,但与上面的ApkVariantData类似,许多值是返回的既定值,并不能自行定义,除了…类似buildConfigField这样的…
发现
回头看最开始使用多渠道时,使用productFlavors时的场景,其中使用buildConfigField来定义一些运行时的常量,以便在各个渠道时各自对应。
productFlavors {
jinxin_bcca {
manifestPlaceholders = [CHANNEL_VALUE: "bcca"]
buildConfigField "String", "BCCA_HOST", '"222.82.20.98"'
buildConfigField "int","BCCA_PORT",'9529'
buildConfigField "int","DEVICE_TYPE",'0x00000631'
}
jinxin_bcca_debug {
manifestPlaceholders = [CHANNEL_VALUE: "bcca_debug"]
buildConfigField "String", "BCCA_HOST", '"39.10.93.118"'
buildConfigField "int","BCCA_PORT",'9529'
buildConfigField "int","DEVICE_TYPE",'0x00000601'
setVersionName("0.888")
}
}
首先找到ProductFlavor这个类,其继承关系如图:
在BaseConfigImpl中是明确可以拿到由buildConfigField定义的值:
public abstract class BaseConfigImpl implements Serializable, BaseConfig {
private final Map<String, ClassField> mBuildConfigFields = Maps.newTreeMap();
@Override
@NonNull
public Map<String, ClassField> getBuildConfigFields() {
return mBuildConfigFields;
}
}
而buildConfigField其实也是一个方法,在BaseFlavor中:
public abstract class BaseFlavor extends DefaultProductFlavor implements CoreProductFlavor {
public void buildConfigField(
@NonNull String type, @NonNull String name, @NonNull String value) {
ClassField alreadyPresent = getBuildConfigFields().get(name);
if (alreadyPresent != null) {
String flavorName = getName();
if (BuilderConstants.MAIN.equals(flavorName)) {
logger.info(
"DefaultConfig: buildConfigField '{}' value is being replaced: {} -> {}",
name,
alreadyPresent.getValue(),
value);
} else {
logger.info(
"ProductFlavor({}): buildConfigField '{}' "
+ "value is being replaced: {} -> {}",
flavorName,
name,
alreadyPresent.getValue(),
value);
}
}
addBuildConfigField(new ClassFieldImpl(type, name, value));
}
}
那么理论上所有在这一系列定义的public方法,其实都可以在build.gradle中的productFlavors中实用,比如这样:
productFlavors {
jinxin_bcca_debug {
manifestPlaceholders = [CHANNEL_VALUE: "bcca_debug"]
buildConfigField "String", "BCCA_HOST", '"39.10.93.118"'
buildConfigField "int","BCCA_PORT",'9529'
buildConfigField "int","DEVICE_TYPE",'0x00000601'
setVersionName("0.888")
}
}
因为DefaultProductFlavor中定义了此方法:
//DefaultProductFlavor.java
@NonNull
public ProductFlavor setVersionName(String versionName) {
mVersionName = versionName;
return this;
}
最终可以使用下面的函数拿到设定的值也就是0.888:
${variant.getProductFlavors()[0].getVersionName()}
其他方法也是类似。
对BuildConfigField而言,理论上是可以这样操作的:
buildConfigField "String", "CUSTOM", '"测试用"'//定义
variant.getProductFlavors()[0].getBuildConfigFields().get("CUSTOM").getValue()//取值
但实际操作中在某种情况下会出现异常,编译器会认为get到的东西为null(哪怕实际编译后并不为null):
Cannot invoke method getValue() on null object
结果
所以经过权衡…目前可以使用VersionNameSuffix,这个方法在BaseConfigImpl中实现,层级足够高,可以被任何ProductFlavor获得:
productFlavors {
jinxin_bcca_debug {
manifestPlaceholders = [CHANNEL_VALUE: "bcca_debug"]
buildConfigField "String", "BCCA_HOST", '"39.10.93.118"'
buildConfigField "int","BCCA_PORT",'9529'
buildConfigField "int","DEVICE_TYPE",'0x00000601'
setVersionNameSuffix("192.168.0.1")
}
}
理论上这个方法只作为versoinName的后缀存在,然后在使用getVersionName()时可以自动获取带后缀的版本名,但现在得避免getVersionName()出现了:
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.buildType.name.equals('release')) {
outputFileName = "SmartStreamGateway_V${defaultConfig.versionName}_${variant.getFlavorName()}_${variant.getProductFlavors()[0].getVersionNameSuffix()}.apk"
} else if (variant.buildType.name.equals('debug')) {
outputFileName = "SmartStreamGateway_V${defaultConfig.versionName}_${variant.getFlavorName()}_${variant.getProductFlavors()[0].getVersionNameSuffix()}_debug.apk"
}
}
}
最终输出的文件名(在debug状态下)是:
SmartStreamGateway_V0.60_jinxin_bcca_debug_192.168.0.1_debug.apk
也可以使用mergedFlavor可以用来应对。
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.buildType.name.equals('release')) {
outputFileName = "SmartStreamGateway_V${defaultConfig.versionName}_${variant.getFlavorName()}_${variant.getMergedFlavor().getVersionNameSuffix()}.apk"
} else if (variant.buildType.name.equals('debug')) {
outputFileName = "SmartStreamGateway_V${defaultConfig.versionName}_${variant.getFlavorName()}_${variant.getMergedFlavor().getVersionNameSuffix()}_debug.apk"
}
}
}
getMergedFalvor会得到这么一个Flavor对象,这个对象是由build.gradle中配置的defaultConfig创建而得的。而这个ProductFlavor同样可以使用getVersionNameSuffix函数得到想要的值。
或者不手动判定buildType和ApplicationId,直接加到文件名中也是可以的:
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "SmartStreamGateway_V${defaultConfig.versionName}_${variant.getFlavorName()}_${variant.getMergedFlavor().getVersionNameSuffix()}_${variant.buildType.name}.apk"
}
}