Android 中实现差异化打包权威指南

一.差异化打包的使用场景

思考:

  1. 一个项目为多个不同的渠道商开发,渠道商都要求显示自己的Logo,怎么设计项目结构 ------某个图片资源不同(或者其他的资源不同)
  2. 如果某一个渠道商表示去掉某一个功能,怎么处理 ------ 某个逻辑判断不同
  3. 如果某一个渠道商需要添加一个自己的宣传页,怎么处理 ------入口不同
  4. 如果渠道商的部分页面不同,怎么组织项目 -------存在逻辑和页面不同
  5. 项目要上线到不同的应用市场,需要统计在不同应用市场的下载情况和使用情况 -----在manifest中配置不同的渠道号

解决以上问题的处理方案:

  1. 新建多个项目-----不利于代码维护,如果项目出错,需要修改所有相关工程
  2. 新建多个分支-----不利于代码维护,如果项目出错,需要修改所有相关分支
  3. 差异化打包 ------ 修改公共部分,一处修改,其他渠道都同步修改

差异化打包用于主线相同,但是同时存在部分功能不同的情况,可以在较小的代码修改的情况下,实现不同的功能


二.差异化打包的使用

1. flavorDimensions多维度要求

flavorDimensions配置多维度的配置渠道

比如现在有一个项目,不同的渠道商的要求不同,而且不同的渠道商又有不同的机型,不同的机型的功能也存在差异。此时仅仅使用渠道商一个维度来差异化打包是不够的,需要通过多维度配置不同版本。

flavorDimensions配置在defaultConfig中, 代码中配置了CHANNELMACHINE_MODE两个维度 ,代码如下

defaultConfig {
    flavorDimensions "CHANNEL", "MACHINE_MODE"
}

注意:

  1. defaultConfig中配置的维度在productFlavors必须全部配置,否则项目会编译不通过
  2. 如果仅仅只有一个维度,flavorDimensions也需要配置,可以配置为default,此时productFlavors中可以不配置维度名称
  3. flavorDimensions中维度具有优先级,优先级是从高到低

2. productFlavors多渠道配置

productFlavors {
    T1 {
        dimension "MACHINE_MODE"
        buildConfigField "String", "MACHINE_CPU", "\"CPU5200\""
        buildConfigField "int", "MACHINE_TYPE", "1"

    }
    T2 {
        dimension "MACHINE_MODE"
        buildConfigField "String", "MACHINE_CPU", "\"CPU5300\""
        buildConfigField "int", "MACHINE_TYPE", "2"
    }


    wandoujia {
        dimension "CHANNEL"
        applicationId = "com.djt.productflavordemo.wandoujia"
        defaultConfig {
            versionNameSuffix "-wandoujia"
            versionName "1.0.20200727.1"
        }
        manifestPlaceholders = [AppName: "豌豆荚", CheckUpdateAppId: "com.djt.productflavordemo.wandoujia"]

    }

    huawei {
        dimension "CHANNEL"
        applicationId = "com.djt.productflavordemo.huawei"
        defaultConfig {
            versionNameSuffix "-huawei"
            versionName "1.0.20200727.2"
        }
        manifestPlaceholders = [AppName: "华为应用商店", CheckUpdateAppId: "com.djt.productflavordemo.wandoujia"]
    }
}

sourceSets {
        
        wandoujia {
            res.srcDirs = ['src/wandoujia/res']
            java {
                srcDirs "src/wandoujia/java"
            }
        }

        huawei {
            res.srcDirs = ['src/huawei/res']
            java {
                srcDirs "src/huawei/java"
            }
        }
    }

以上配置的productFlavors生成的版本如下:

android 制作差分包命令 android差异化打包_gradle差异化打包

(1)flavorDimensionsproductFlavors的关系:

在AS中打开Build Variants构建差异化视图,左侧是模块,右侧是模块存在的差异化版本列表,可以选择一个变体版本作为当前运行的版本。

版本名称命名规则与flavorDimensions配置的维度优先级有关::渠道名称+机型+Debug/Release

渠道名称和机型的先后顺序与flavorDimensionsCHANNELMACHINE_MODE的优先级一致

每一个版本必须且仅能配置一个维度,否则编译失败

(2)productFlavors中配置的值与BuildConfig之间的对应关系


public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.djt.productflavordemo.huawei";  //应用的包名,对应`productFlavors`的applicationId
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "huaweiT1"; //变体的名称:"CHANNEL"+"MACHINE_MODE",与`flavorDimensions`保持一致
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0.20200727.2-huawei"; //versionName和 versionNameSuffix结合
  public static final String FLAVOR_CHANNEL = "huawei"; //所属渠道名称
  public static final String FLAVOR_MACHINE_MODE = "T1";//所属机型名称
  // Fields from product flavor: T1
  public static final String MACHINE_CPU = "CPU5200"; //productFlavors配置的String类型常量值
  public static final int MACHINE_TYPE = 1;//productFlavors配置的int常量值
}

注意:

BuildConfig文件中的常量值在productFlavors配置的时候,int类型的值必须加双引号,String类型的值必须添加转义符号的双引号。

T1 {
        dimension "MACHINE_MODE"
        buildConfigField "String", "MACHINE_CPU", "\"CPU5200\""
        buildConfigField "int", "MACHINE_TYPE", "1"
    }

(3)sourceSets 配置各变体的代码文件的路径和资源文件的路径

需要在main文件夹同层级文件夹新建wandoujiahuawei文件夹,文件夹的内部文件结构要和main文件夹的子文件结构保持一致,但是仅仅需要新建将要使用到的文件结构。

sourceSets {
  
    wandoujia {
        res.srcDirs = ['src/wandoujia/res']
        java {
            srcDirs "src/wandoujia/java"
        }
    }

    huawei {
        res.srcDirs = ['src/huawei/res']
        java {
            srcDirs "src/huawei/java"
        }
    }
}
Java文件树对比

android 制作差分包命令 android差异化打包_差异化打包_02


res文件树对比

android 制作差分包命令 android差异化打包_gradle差异化打包_03


(4)使用versionNameSuffix的优点

不同的渠道商编译的APK的版本号都添加相应渠道商的后缀,方便测试人员识别多个渠道编译的APK,以免APK混淆

3. 代码文件如何实现差异化

使用场景:该版本与主线版本逻辑基本相同,只有部分逻辑不同,可以获取BuildConfig的值进行区分当前版本

代码如下:

val sb = StringBuffer()
when (BuildConfig.FLAVOR_MACHINE_MODE) {

    "T1" -> Toast.makeText(
        this@MainActivity,
        "机器名称:${BuildConfig.FLAVOR_MACHINE_MODE}",
        Toast.LENGTH_SHORT
    ).show()
    "T2" -> Toast.makeText(
        this@MainActivity,
        "机器名称:${BuildConfig.FLAVOR_MACHINE_MODE}",
        Toast.LENGTH_SHORT
    ).show()
    "T3" -> Toast.makeText(
        this@MainActivity,
        "机器名称:${BuildConfig.FLAVOR_MACHINE_MODE}",
        Toast.LENGTH_SHORT
    ).show()

}
sb.append("机器名称:${BuildConfig.FLAVOR_MACHINE_MODE}\n")
if (BuildConfig.MACHINE_TYPE == 1) {
    sb.append("机型:${BuildConfig.MACHINE_TYPE}\n")
} else {
    sb.append("机型:${BuildConfig.MACHINE_TYPE}\n")
}

if (BuildConfig.MACHINE_CPU == "CPU5200") {
    sb.append("机型:${BuildConfig.MACHINE_CPU}\n")
} else {

}
sb.append(MetaUtils.getMetaStrValue(this, "CheckUpdateAppId")+"\n")
sb.append(getString(R.string.app_name))

content.text = sb.toString()

使用场景:该版本部分类与主线版本差异较大,需要把整个类提取出来。

把提取出来的类,保存在对应的模块的对应文件夹下。

如果在某一个模块下提取了某个文件为差异化部分,那么在所有的差异化模块都要把这个文件提取出来

4. 资源文件如何实现差异化

如果仅仅是资源文件不同,直接在res对应的文件夹下面新建一个文件相同文件名的文件就好。在实际打包过程中,差异化文件夹下面的res文件会和main主线的res文件合并,APK在实际调用过程中,会优先调用差异化包中的文件,在差异化包中没有对应文件或者资源,会调用main主线中的资源

适用范围:

适用所有的res文件

在适配图片的时候,必须在将图片放在适配机器对应的尺寸文件下,否则会调用用main主线中的资源

5. AndroidManifest文件如何实现差异化

manifest文件也可以实现差异化,在main主线中manifest属于所有项目的通用部分,变体版本文件下的manifest属于各自项目的独有部分。

main主线的manifest和变体版本文件下manifest配置不能冲突,否则编译会报错

main主线的manifest和变体版本文件下manifest配置组件的不同属性,结果是两个manifest的合并


三.其他与AndroidManifest内容补充:

(1)如何在AndroidManifest中使用build.gradle文件的赋值

meta-data元素下通过${}引用

<meta-data
    android:name="CheckUpdateAppId"
    android:value="${CheckUpdateAppId}" />

build.gradle中使用manifestPlaceholders赋值,manifestPlaceholders接受的是一个数组类型,格式[常量名称1:常量值1,常量名称2,常量值2]

huawei {
    dimension "CHANNEL"
    applicationId = "com.djt.productflavordemo.huawei"
    defaultConfig {
        versionNameSuffix "-huawei"
        versionName "1.0.20200727.2"
    }
    manifestPlaceholders = [AppName: "华为应用商店", CheckUpdateAppId: "com.djt.productflavordemo.wandoujia"]

(2)如何在代码块中使用AndroidManifest中的meta-data节点的数据

ApplicationInfometaData返回的是一个Bundle类型的对象,可以根据meta-data中要获取的值的类型,来调用Bundle对应的获取相应类型的值的方法。代码中定义了获取Stringintboolean类型值的方法

/**
 * 获取meta的所有值
 * @param context
 * @return
 */
public static Bundle getMetaBundle(Context context) {
    try {
        ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
        return info.metaData;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

/**
 * 获取meta的Str值
 * @param context
 * @param keyName
 * @return
 */
public static String getMetaStrValue(Context context, String keyName) {
    try {
        Bundle metaBundle = getMetaBundle(context);
        if(metaBundle != null) {
            String msg = metaBundle.getString(keyName);
            if (!TextUtils.isEmpty(msg)) {
                return msg;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "";
}

 /**
     * 获取meta的Int值
     * @param context
     * @param keyName
     * @return
     */
    public static int getMetaIntValue(Context context, String keyName, int defaultValue) {
        try {
            Bundle metaBundle = getMetaBundle(context);
            if(metaBundle != null) {
                return metaBundle.getInt(keyName, defaultValue);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return defaultValue;
    }

    /**
     * 获取meta的Boolean值
     * @param context
     * @param keyName
     * @return
     */
    public static boolean getMetaBooleanValue(Context context, String keyName, boolean defaultValue) {
        try {
            Bundle metaBundle = getMetaBundle(context);
            if(metaBundle != null) {
                return metaBundle.getBoolean(keyName, defaultValue);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return defaultValue;
    }

注意:

meta-data可以动态获取,但是不可以在代码中动态设置。虽然我们可以获取 ApplicationInfometaData返回的是一个Bundle类型的对象,并可以对Bundle进行赋值,但是仅仅是修改了Bundle对象的值,实际获取meta-data的值不会修改。

(3)如何实现一个APK拥有多个入口,即在桌面上显示不同的图标和应用名

  • 为需要作为入口的Activity添加Launcher属性;
  • Activity添加label值和icon值,分别为入口名称和图标
<activity
    android:name=".MainActivity"
    android:label="豌豆荚"
    android:icon="@drawable/ic_launcher_foreground">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

如果不配置label值和icon值,则应用的图标和名称与Application中配置的相同