Android 中实现差异化打包权威指南
一.差异化打包的使用场景
思考:
- 一个项目为多个不同的渠道商开发,渠道商都要求显示自己的Logo,怎么设计项目结构 ------某个图片资源不同(或者其他的资源不同)
- 如果某一个渠道商表示去掉某一个功能,怎么处理 ------ 某个逻辑判断不同
- 如果某一个渠道商需要添加一个自己的宣传页,怎么处理 ------入口不同
- 如果渠道商的部分页面不同,怎么组织项目 -------存在逻辑和页面不同
- 项目要上线到不同的应用市场,需要统计在不同应用市场的下载情况和使用情况 -----在manifest中配置不同的渠道号
解决以上问题的处理方案:
-
新建多个项目-----不利于代码维护,如果项目出错,需要修改所有相关工程
-
新建多个分支-----不利于代码维护,如果项目出错,需要修改所有相关分支
-
差异化打包 ------ 修改公共部分,一处修改,其他渠道都同步修改
差异化打包用于主线相同,但是同时存在部分功能不同的情况,可以在较小的代码修改的情况下,实现不同的功能
二.差异化打包的使用
1. flavorDimensions
多维度要求
flavorDimensions
配置多维度的配置渠道
比如现在有一个项目,不同的渠道商的要求不同,而且不同的渠道商又有不同的机型,不同的机型的功能也存在差异。此时仅仅使用渠道商一个维度来差异化打包是不够的,需要通过多维度配置不同版本。
flavorDimensions
配置在defaultConfig
中, 代码中配置了CHANNEL
和 MACHINE_MODE
两个维度 ,代码如下
defaultConfig {
flavorDimensions "CHANNEL", "MACHINE_MODE"
}
注意:
defaultConfig
中配置的维度在productFlavors
必须全部配置,否则项目会编译不通过如果仅仅只有一个维度,
flavorDimensions
也需要配置,可以配置为default
,此时productFlavors
中可以不配置维度名称
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
生成的版本如下:
(1)flavorDimensions
和productFlavors
的关系:
在AS中打开
Build Variants
构建差异化视图,左侧是模块,右侧是模块存在的差异化版本列表,可以选择一个变体版本作为当前运行的版本。
版本名称命名规则与flavorDimensions
配置的维度优先级有关::渠道名称+机型+Debug/Release
渠道名称和机型的先后顺序与flavorDimensions
中CHANNEL
和 MACHINE_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
文件夹同层级文件夹新建wandoujia
和huawei
文件夹,文件夹的内部文件结构要和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文件树对比
res文件树对比
(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
节点的数据
ApplicationInfo
的metaData
返回的是一个Bundle
类型的对象,可以根据meta-data
中要获取的值的类型,来调用Bundle
对应的获取相应类型的值的方法。代码中定义了获取String
,int
和boolean
类型值的方法
/**
* 获取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
可以动态获取,但是不可以在代码中动态设置。虽然我们可以获取 ApplicationInfo
的metaData
返回的是一个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
中配置的相同
参考引用:
谷歌官方productFlavors传送门