美文网首页
手游海外SDK实战——Android客户端之动态插件

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

作者: U8SDK | 来源:发表于2021-02-24 09:46 被阅读0次

    一、前言

    随着国内手游版号申请难度的增加,以及防沉迷等一系列政策的影响,很多国内开发者纷纷开始寻求海外发行之路。那么手游出海首要的是需要一套适合海外发行和运营的手游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中动态插件的设计。

    相关文章

      网友评论

          本文标题:手游海外SDK实战——Android客户端之动态插件

          本文链接:https://www.haomeiwen.com/subject/ofqcxltx.html