单工程多app实现指南

1,563 阅读4分钟

一.背景:

目前我们的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 {
    }
}

到此完成工程的基本配置,可以完美打不同特性的包了.