手游海外SDK实战——Android客户端之动态插件

2021-02-24  本文已影响0人  U8SDK

一、前言

随着国内手游版号申请难度的增加,以及防沉迷等一系列政策的影响,很多国内开发者纷纷开始寻求海外发行之路。那么手游出海首要的是需要一套适合海外发行和运营的手游SDK联运系统。

本系列我们就来开发一套这样的SDK,我们暂且称这套SDK为UGSDK。

整个UGSDK项目,暂时可以分为三大部分——Android客户端SDK部分、iOS客户端SDK部分以及服务端部分(目前不考虑H5游戏部分)。

本篇主要介绍UGSDK项目中Android客户端部分中的插件接入设计以及插件的动态配置。

二、插件设计

1、插件设计思路

手游SDK功能中,除了基础且必要的功能(比如登陆和支付)外,还需要接入一些其他第三方插件,比如广告归因和数据统计插件Appsflyer或者Adjust等,其他的还有比如分享、推送、客服等插件。

因为是第三方插件SDK,有时候根据运营需求,我们可能会替换或者接入多个同类型的插件,比如Appsflyer和Ajust。或者可能游戏那边也接入了这些插件, 我们需要方便地删除这些插件。

所以,在设计插件的时候, 我们尽可能采用动态配置的方式。 这样不仅有利于插件的扩展和删除,后面还可以写辅助工具基于apk来删除或者替换其中的插件,而不需要游戏研发重新出包。

2、插件设计实例

我们以广告归因插件为例,比如我们要接入Appsflyer插件。 我们在SDK中或者给游戏层提供的调用接口,并不直接调用Appsflyer插件的api。为了让调用层不需要关心具体调用的是哪个插件, 我们抽象出一个和具体插件无关的接口。

比如广告归因插件, 我们定义一个抽象接口,根据业务需求,将可能需要调用的API抽象一下:

/**
 * 统计/广告归因 插件接口
 */
public interface IAnalytics extends IPlugin {

    String TYPE = "analytics";

    /**
     * 自定义事件: SDK初始化开始
     */
    void onInitBegin();

    /**
     * 自定义事件: SDK初始化成功
     */
    void onInitSuc();

    /**
     * 自定义事件: SDK登陆开始
     */
    void onLoginBegin();

    /**
     * 登陆成功的时候 上报
     * af_login
     */
    void onLogin();

    /**
     * 注册成功的时候 上报
     * af_complete_registration
     */
    void onRegister(int regType);

    /**
     * 自定义事件, 开始购买(SDK下单成功)
     * price:分为单位
     */
    void onPurchaseBegin(UGOrder order);

    /**
     * 购买成功的时候,调用
     * af_purchase
     * price: 分为单位
     */
    void onPurchase(UGOrder order);



    /**
     * 自定义事件, 创建角色成功
     * @param role
     */
    void onCreateRole(UGRoleData role);

    /**
     * 自定义事件, 进入游戏成功
     */
    void onEnterGame(UGRoleData role);

    /**
     * 角色等级 升级的时候,调用
     * af_level_achieved
     */
    void onLevelup(UGRoleData role);

    /**
     * 完成新手教程的时候 执行
     * af_tutorial_completion
     * @param tutorialID
     * @param content
     */
    void onCompleteTutorial(int tutorialID, String content);

    /**
     * 自定义上报
     * @param eventName
     * @param params
     */
    void onCustomEvent(String eventName, Map<String, Object> params);

}

根据上面可见,我们定义了一个和具体插件无关的IAnalytics接口, 然后我们给插件调用封装一个UGAnalytics单例类。 SDK中或者游戏层需要调用统计插件接口的话,都调用UGAnalytics单例类中的方法:

public class UGAnalytics {

    private static UGAnalytics instance;

    private Map<String, PluginInfo> plugins;
    private List<IAnalytics> analyticPlugins;

    private UGAnalytics() {
        plugins = new HashMap<>();
        analyticPlugins = new ArrayList<>();
    }

    public static UGAnalytics getInstance() {
        if(instance == null) {
            instance = new UGAnalytics();

        }
        return instance;
    }

    /**
     * 添加统计插件的实现
     * @param plugin
     */
    public void registerPlugin(PluginInfo plugin) {
        if(plugin == null || plugin.getPlugin() == null) {
            Log.w(Constants.TAG, "registerPlugin in UGAnalytics failed. plugin is null");
            return;
        }

        if(!(plugin.getPlugin() instanceof IAnalytics)) {
            Log.w(Constants.TAG, "registerPlugin in UGAnalytics failed. plugin is not implement IAnalytics");
            return;
        }

        if(!plugins.containsKey(plugin.getClazz())) {
            plugins.put(plugin.getClazz(), plugin);
            analyticPlugins.add((IAnalytics)plugin.getPlugin());
        }
    }

    public void onInitBegin() {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onInitBegin();
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onInitBegin failed." + plugin.getClass().getName());
            }

        }
    }

    public void onInitSuc() {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onInitSuc();
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onInitSuc failed." + plugin.getClass().getName());
            }

        }
    }

    public void onLoginBegin() {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onLoginBegin();
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onLoginBegin failed." + plugin.getClass().getName());
            }

        }
    }

    public void onLogin() {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onLogin();
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onLogin failed." + plugin.getClass().getName());
            }

        }
    }

    public void onRegister(int regType) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onRegister(regType);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onRegister failed." + plugin.getClass().getName());
            }

        }
    }

    public void onPurchaseBegin(UGOrder order) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onPurchaseBegin(order);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onPurchaseBegin failed." + plugin.getClass().getName());
            }

        }
    }

    public void onPurchase(UGOrder order) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onPurchase(order);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onPurchase failed." + plugin.getClass().getName());
            }

        }
    }

    public void onCreateRole(UGRoleData role) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onCreateRole(role);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onCreateRole failed." + plugin.getClass().getName());
            }

        }
    }

    public void onEnterGame(UGRoleData role) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onEnterGame(role);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onEnterGame failed." + plugin.getClass().getName());
            }

        }
    }

    public void onLevelup(UGRoleData role) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onLevelup(role);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onLevelup failed." + plugin.getClass().getName());
            }

        }
    }

    public void onCompleteTutorial(int tutorialID, String content) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onCompleteTutorial(tutorialID, content);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onCompleteTutorial failed." + plugin.getClass().getName());
            }

        }
    }

    public void onCustomEvent(String eventName, Map<String, Object> params) {
        for(IAnalytics plugin : analyticPlugins) {
            try {
                plugin.onCustomEvent(eventName, params);
            }catch (Exception e){
                e.printStackTrace();
                Log.e(Constants.TAG, "analytic plugin onCustomEvent failed." + plugin.getClass().getName());
            }

        }
    }
}

上面UGAnalytics中,我们维护了一个IAnalytics插件列表, 因为考虑到后面我们可能同时接入多个广告归因插件, 所以我们的统计插件设计让他可以支持多个同类型的插件。

另外就是一个插件注册方法registerPlugin, 当程序启动的时候, 我们通过这个函数注入对应的插件配置信息。

其他的接口就和IAnalytics中定义的一致了, 所有的接口里面,都是循环调用了每个具体的统计插件实现类的对应api。这样,哪怕我们一个统计插件都没有接入或者打进apk包里面, 调用UGAnalytics中对应api的地方,也不会有任何问题。

三、动态配置和加载

1、 插件配置

为了实现插件的动态配置和可插拔,除了上面的插件抽象之外, 我们需要将插件的配置放到外部配置文件,而不是写死在代码中。

我们在assets目录下定义一个插件配置文件:我们定义一下插件的配置文件ug_plugins.json, 里面的配置内容和格式如下:

[
  {
    "type": "analytics",
    "name": "appsflyer",
    "class": "com.ug.sdk.plugin.appsflyer.UGAppsflyer",
    "params": {
      "dev.key": "22222"
    }
  },
  {
    "type": "analytics",
    "name": "adjust",
    "class": "com.ug.sdk.plugin.adjust.UGAdjust",
    "params": {
      "app.key": "11111"
    }
  },
  ...
]

每个插件对应一个json配置块, 我们对其中每一项做一个简单的说明:

1、type: 插件类型, 为不同的插件定义一个唯一的插件类型,比如广告归因/统计定义为analytics,分享插件定义为shares等。
2、name: 插件名称。
3、class:插件实现类的全名, 对于广告归因插件就是实现了IAnalytics接口的插件实现类。
4、params: 该插件需要的额外配置参数,比如该插件的appid、appkey等参数。

2、动态加载和初始化

上面定义好了配置文件之后, 那么程序启动的时候, 我们需要解析这个插件配置文件, 然后通过反射的形式,完成各个插件实现类的实例化和初始化,也是非常简单,我们直接看代码:

    /**
     * 注册所有插件,并实例化
     * @param context
     */
    public void initPlugins(Context context) {

        List<PluginInfo> plugins = loadFromFile(context);
        for(PluginInfo plugin : plugins) {

            if(plugin.getPlugin() == null) {
                Log.e(Constants.TAG, "plugin instance failed." + plugin.getClazz());
                continue;
            }

            Log.d(Constants.TAG, "begin to register a new plugin type:" + plugin.getType() + "; class:" + plugin.getClazz());

            if(IAnalytics.TYPE.equals(plugin.getType())) {
                //注册统计插件
                UGAnalytics.getInstance().registerPlugin(plugin);
            }
            //TODO: 后面如果有其他插件类型, 也继续在这里完成该类型插件的注册

            //插件初始化
            plugin.getPlugin().init(context, plugin.getParams());
        }
    }

这样设计之后, 所有插件接口的调用和插件接口的实现就完全解耦了。 调用者不需要关心调用的具体是什么插件,插件实现类也不需要关心上层被SDK或者被哪个游戏调用。

而且基于这样的设计,我们可以很方便的完成插件的替换。 比如我们给游戏接入的时候,统计插件接入的是Adjust,现在因为运营调整,需要出一个接了Appsflyer的游戏包。 那么基于U8SDK一样的打包原理,我们不需要让游戏研发重新接SDK和换包,我们自己就可以完成插件的替换了。

好了,本篇我们介绍了在UGSDK中动态插件的设计。

上一篇下一篇

猜你喜欢

热点阅读