关于gradle多渠道打包的命名

使用gradle多渠道打包也不是什么新鲜事了,配置productFlavors就完事了,再写点buildConfigField什么的,似乎也就可以用了。

用确实是可以用,但遇上某天想改打包出来的名字就很尴尬了,不知道怎么改。

使用

本来在build.gradle有这样的配置:

android {
    productFlavors {
        jinxin_bcca {
            manifestPlaceholders = [CHANNEL_VALUE: "bcca"]
            buildConfigField "String", "BCCA_HOST", '"222.182.202.98"'
            buildConfigField "int","BCCA_PORT",'9529'
            buildConfigField "int","DEVICE_TYPE",'0x00000631'
        }
        jinxin_bcca_debug {
            manifestPlaceholders = [CHANNEL_VALUE: "bcca_debug"]
            buildConfigField "String", "BCCA_HOST", '"39.105.93.18"'
            buildConfigField "int","BCCA_PORT",'9529'
            buildConfigField "int","DEVICE_TYPE",'0x00000601'
        }
    }
}

然后加一段配置代码:

            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> {

    /**
     * Executes the given action against all objects in this collection, and any objects subsequently added to this
     * collection.
     *
     * @param action The action to be executed
     */
    void all(Action<? super T> action);

    /**
     * Executes the given closure against all objects in this collection, and any objects subsequently added to this collection. The object is passed to the closure as the closure
     * delegate. Alternatively, it is also passed as a parameter.
     *
     * @param action The action to be executed
     */
    void all(Closure action);
}

这是一个实打实的Java接口。

“Groovy编译器产生的字节码和Java编译器产生的字节码是完全一样的。这就等于说,Groovy能够完全使用各种Java API。” ----《Groovy入门经典》

有了这种认知,改配置代码其实就和改Java代码一样了。

常用到的一些方法在接口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.182.202.98"'
            buildConfigField "int","BCCA_PORT",'9529'
            buildConfigField "int","DEVICE_TYPE",'0x00000631'
        }
        jinxin_bcca_debug {
            manifestPlaceholders = [CHANNEL_VALUE: "bcca_debug"]
            buildConfigField "String", "BCCA_HOST", '"39.105.93.18"'
            buildConfigField "int","BCCA_PORT",'9529'
            buildConfigField "int","DEVICE_TYPE",'0x00000601'
            setVersionName("0.888")
        }
    }

首先找到ProductFlavor这个类里的方法,其继承关系如图:
关于gradle多渠道打包的命名
在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.105.93.18"'
            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:

    productFlavors {
        jinxin_bcca_debug {
            manifestPlaceholders = [CHANNEL_VALUE: "bcca_debug"]
            buildConfigField "String", "BCCA_HOST", '"39.105.93.18"'
            buildConfigField "int","BCCA_PORT",'9529'
            buildConfigField "int","DEVICE_TYPE",'0x00000601'
            setVersionNameSuffix("192.168.0.1")
        }
    }

只要不与variant.getVersionName()同时使用即可,因为setVersionNameSuffix会让variant.getVersionName()返回VersionNameSuffix(有点坑)。那么最终打包可以这么使用了:

    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

最好以后能想到更好的办法,不然会因为强迫症发作而疯掉。

上一篇:融云即时通讯sdk, 把头像设置为圆角图片在4.x和5.x的区别 -- 融云即时聊天sdk使用小技巧


下一篇:页面滚动,底部视图显示和隐藏详解 android