關於gradle多渠道打包的命名

使用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這個類,其繼承關係如圖:
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"
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章