一.背景:
目前我们的app主要是在googlePlay上架, 为了扩大覆盖面,我们需要在华为,小米等海外商店上架自己的App,但因为是海外商店的原因,要求包名id不一样,也就是一个新的app。
二.存在问题:
我们目前的工程配置是实现不了了,乃至于在好几次给华为,小米单独打包的时候,都是拉取之前配置好代码的华为、小米分支。单独打包。这导致每次打包都很费时间,需要切分支,而且无法没有影响的合入不断迭代的功能。
在googleplay商店和华为商店的App在功能上也有所不同,主要是登录服务体系与sku购置,支付体系不同。因此在华为商店的App我们停止更新了一年之久,中间将近30个版本的功能都没有并入。
三.解决方法:
- 工程构建方案
经过调研,我决定采用多渠道,多包名配置的方案,通过Gradle文件的配置来实现分渠道与包;通过抽象接口服务来实现相同业务,不同服务支持的实现。
- 业务处理三方问题
因为我们的App集成了facebook与GoogleAD,做了验证后,目前因修改包名,不会影响这两个服务的使用。因此,在业务上的处理我们的重点都在登录逻辑和支付体系上。
多产品配置
1.gradle配置
在主module的build.gradle配置app维度,productFlavors可配置多个不同特性的app,如下我们配置普通的app和华为特性的app,我们在这里配置app的id,渠道信息,最终打出的包他们的这些信息都会不一样,类似应用多开的感觉(当前,它们的实现机制截然不同,应用双开的技术未有了解),有两种方案可供选择:
A方案:通过applicationIdSuffix属性来配置华为的包名, applicationIdSuffix
标识默认的应用 ID 上追加一段,在最后构建后,它会将所有的包名都替换为新的,包名无法完全替换,只能在尾部追加名称
//google play store 与 huaWei store 配置
flavorDimensions "app"
productFlavors {
demo {
dimension "app"
manifestPlaceholders = [YOGA_CHANNEL_NAME:"google", YOGA_CHANNEL_CODE:"600001"]
}
huaWei {
dimension "app"
applicationIdSuffix ".huawei"
manifestPlaceholders = [YOGA_CHANNEL_NAME:"h2o_huawei", YOGA_CHANNEL_CODE:"300001"]
}
}
B方案:通过配置全新的applicationId来更改包名,可以完全替换包名,B方案看起来清晰明了一些,我们选择B方案:
//google play store 与 huaWei store 配置
flavorDimensions "app"
productFlavors {
demo {
dimension "app"
applicationId "com.demo.inc"
manifestPlaceholders = [YOGA_CHANNEL_NAME:"google", YOGA_CHANNEL_CODE:"600001"]
}
huaWei {
dimension "app"
applicationId "com.demo.inc.huawei"
manifestPlaceholders = [YOGA_CHANNEL_NAME:"h2o_huawei", YOGA_CHANNEL_CODE:"300001"]
}
}
defaultConfig中可以不需要配置基准applicationId
defaultConfig {
minSdkVersion 17
targetSdkVersion 28
versionCode 1
versionName "1.0.00"
multiDexEnabled true
}
接下来需要配置sourceSets,表示不同特性的app提供的资源:
sourceSets {
huaWei {
java.srcDirs = ['src/huaWei/java']
}
}
这里我们只需要配置java.srcDirs,只提供一些代码服务,如果需要其他资源,可以使用res来增加非代码资源,示例如下:
sourceSets {
huaWei {
res.srcDirs = ['src/huaWei/res']
}
}
然后在工程主app下创建与main同级的huaWei目录即可。目录结构大概这样:
app
--src
--huaWei
--java
--main
--java
2.AndroidManifest配置
我们在多产品配置中配置了不同app的applicationId,还需要在AndroidManifest 配置Provider的authorities
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
在修改应用 ID的时候,我们需要注意下AndroidManifest.xml中的package属性
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.demo.inc">
Android 构建工具会将 package
属性用于下面两方面:
-
它会将此名称用作应用生成的 R.java类的命名空间。
示例:对于上面的清单,
R
类将为com.demo.inc.R
。 -
它会使用此名称解析清单文件中声明的任何相关类名称。
示例:对于上面的清单,声明为
<activity android:name=".MainActivity">
的 Activity 将解析为com.demo.inc.MainActivity
。
我们不需要修改package属性。
最后,我们基于com.demo.inc的动态跳转将完全失效,我们需要在动态跳转根据包的特性来替换下发的类全称,恢复在华为特性app上的动态跳转。
3.渠道区分
我们有一些业务场景,需要区分渠道来源做一些工作,下面我们来实现它。
首先在主项目AndroidManifest中加入刚才在build.gradle中配置的渠道标签
<!-- 渠道名称 -->
<meta-data
android:name="YOGA_CHANNEL_NAME"
android:value="${YOGA_CHANNEL_NAME}" />
<!-- 渠道号 -->
<meta-data
android:name="YOGA_CHANNEL_CODE"
android:value="${YOGA_CHANNEL_CODE}" />
然后在需要知道渠道的地方获取。当前我们项目的渠道配置方式,手动设置为 “google”
//神策基础参数
properties.put(ConstServer.CHANNEL, "google");
properties.put("AppName", "demo");
但现在需要区分,因此我们需要处理渠道的获取,以渠道名称为例:
创建获取渠道方法:
public static String getChannelName() {
String channelName = null;
if (BuildConfig.DEBUG) {
return getChannelNameByFlavor();
}
try {
PackageManager pm = YogaInc.getInstance().getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(YogaInc.getInstance().getPackageName(), PackageManager.GET_META_DATA);
channelName = appInfo.metaData.getString("YOGA_CHANNEL_NAME");
} catch (Exception ignored){}
if (TextUtils.isEmpty(channelName)) {
channelName =getChannelNameByFlavor();
}
return channelName;
}
在Debug环境中因为还无法从AndroidManifest获取,因此我们依赖FLAVOR.来获取,保证测试阶段能够区分。getChannelNameByFlavor的处理:
private static String getChannelNameByFlavor() {
if (BuildConfig.FLAVOR.equals("demo")) {
return "google";
} else {
return "h2o_huawei";
}
}
最后在设置渠道处动态获取
properties.put(ConstServer.CHANNEL, CommonUtil.getChannelName());
依赖代码隔离
两个不同app因为所需服务不同,因此依赖的包也不同,我们需要根据实际情况来对包做一些隔离,一般的有插件隔离和包依赖隔离。
1.插件隔离
因为华为服务接入时,需要引入插件来检测包的一些信息,当我们打包时,插件发现apk包名对不上就会报错,导致无法打包。
同样的,谷歌服务的插件是此机制,因此我们可以在gradle中做区分引入,根据task的名称来动态区分
//判断当前产品类型,应用不同的插件
def getCurrentFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
if(tskReqStr.contains("demo")){
apply plugin: 'com.google.gms.google-services'
}else {
apply plugin: 'com.huawei.agconnect'
}
}
2.包依赖隔离
为了在打google play上架的包时,不依赖华为服务的代码,我们在依赖华为包时通过
productFlavors的Flavor+Implementation来依赖:
//huWei包依赖
huaWeiImplementation 'com.huawei.hms:hwid:5.0.1.300'
这里有个细节需要注意,如果需要这种形式的依赖,那么Flavor的命名必须为驼峰标识,也就单词中包含一个大写字母,开头大写也不OK,比如abcEcc:
productFlavors {
abcEcc {
}
}
到此完成工程的基本配置,可以完美打不同特性的包了.