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