手游海外SDK实战——Android客户端之插件篇
一、前言
随着国内手游版号申请难度的增加,以及防沉迷等一系列政策的影响,很多国内开发者纷纷开始寻求海外发行之路。那么手游出海首要的是需要一套适合海外发行和运营的手游SDK联运系统。
本系列我们就来开发一套这样的SDK,我们暂且称这套SDK为UGSDK。该SDK已经开发完成,如果有兴趣或者想体验完整功能的同学,可以加我们的海外技术交流QQ群:1055996444。
整个UGSDK项目,暂时可以分为三大部分——Android客户端SDK部分、iOS客户端SDK部分以及服务端部分(目前不考虑H5游戏部分)。
本篇主要介绍UGSDK项目中Android客户端部分中的插件接入设计以及插件的动态配置。
二、插件设计
1、插件设计思路
手游SDK功能中,除了基础且必要的功能(比如登陆和支付)外,还需要接入一些其他第三方插件,比如广告归因和数据统计插件Appsflyer或者Adjust等,其他的还有比如分享、推送、客服等插件。
因为是第三方插件SDK,有时候根据运营需求,我们可能会替换或者接入多个同类型的插件,比如Appsflyer和Ajust。或者可能游戏那边也接入了这些插件, 我们需要方便地删除这些插件。
所以,在设计插件的时候, 我们尽可能采用动态配置的方式。 这样不仅有利于插件的扩展和删除,后面还可以写辅助工具基于apk来删除或者替换其中的插件,而不需要游戏研发重新出包。
2、插件设计实例
我们以广告归因插件为例,比如我们要接入Appsflyer插件。 我们在SDK中或者给游戏层提供的调用接口,并不直接调用Appsflyer插件的api。为了让调用层不需要关心具体调用的是哪个插件, 我们抽象出一个和具体插件无关的接口。
比如广告归因插件, 我们定义一个抽象接口,根据业务需求,将可能需要调用的API抽象一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
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单例类中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
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, 里面的配置内容和格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[ { "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 2 3 4 5 6 |
1、type: 插件类型, 为不同的插件定义一个唯一的插件类型,比如广告归因/统计定义为analytics,分享插件定义为shares等。 2、name: 插件名称。 3、class:插件实现类的全名, 对于广告归因插件就是实现了IAnalytics接口的插件实现类。 4、params: 该插件需要的额外配置参数,比如该插件的appid、appkey等参数。 |
2、动态加载和初始化
上面定义好了配置文件之后, 那么程序启动的时候, 我们需要解析这个插件配置文件, 然后通过反射的形式,完成各个插件实现类的实例化和初始化,也是非常简单,我们直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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中动态插件的设计。感兴趣的同学可以加入我们的技术交流Q裙哦(1055996444)。
本文出自 U8SDK技术博客,转载时请注明出处及相应链接。
本文永久链接: http://www.uustory.com/?p=2415