各位看官,上文 手游SDK— 第二篇(架构设计篇)已经介绍过了架构实现的基本思路,那废话就不多说了,进入正题,看下代码的具体实现。
PS:不具备实际项目用途,仅做框架Demo展示
项目结构搭建
首先咱们先整体搭建下项目工程的框架模块,根据之前的项目架构设计搭建的,这里也比较简单,我简单附上一张图。
PS:相关的模块管理可参考
- Android Studio 多Module统一管理方法
项目代码实现
第一部分:基础库
基础库涉及的功能库比较多,大概抽离部分模块大体讲解下,主要分两部分:涉及SDK架构的代码实现和部分三方框架的封装思路讲解。
项目/插件/渠道管理设计
项目的框架主体骨架
配置文件格式:Project_config.txt / Plugin_config.txt / Channel_config.txt
{
"project": [
{
"project_name": "project",
"class_name": "com.bzai.gamesdk.project.juhe.JuHeProject",
"description": "聚合SDK项目",
"version": "1.0.0"
}
]
}
{
"plugin": [
{
"plugin_name": "plugin_wechat",
"class_name": "com.bzai.gamesdk.plugin.wechat.WechatPlugin",
"description": "微信功能插件",
"version": "5.1.4"
},
{
"plugin_name": "plugin_alipay",
"class_name": "com.bzai.gamesdk.plugin.alipay.AlipayPlugin",
"description": "支付宝功能插件",
"version": "15.5.5"
}
]
}
{
"channel": [
{
"channel_name": "channel",
"class_name": "com.bzai.gamesdk.channel.test.TestChannelSDK",
"description": "测试渠道SDK",
"version": "1.0.0"
}
]
}
比较简单就不细说,是Json格式的数据,大家可以根据需求进行数据拓展。
PS:备注说明下,其实是可以通过一个配置文件就能配置完的,但是将项目、插件、渠道分别配置加载的目的是方便快速的分别替换项目、插件、渠道配置。一个项目Project可以对应多个渠道、多个插件。后续可以在多渠道、多插件上进行快速的插拔和后台开关切换渠道,不过正常的需求都是一个项目对应零个或一个渠道、零个或多个功能插件。
代码实现
抽象类解析
1、抽象的项目Project类,主要是面向SDKAPI接口设计的,预定义对外的接口及Activity生命周期接口。但是通常对外的接口一般设计好后非必要就很少修改,为了对外接口设计进行扩展,预定义通用的拓展接口extendFunction(Activity activity, int functionType, Object object, CallBackListener callBackListener),通过不同的funtionType进行扩展。
public abstract class Project implements ProguardInterface{
/***************************** Project 加载接口 **********************************/
public ProjectBeanList.ProjectBean projectBean;
private boolean hasInited;
protected synchronized void initProject() {
if (hasInited) {
return;
}
hasInited = true;
}
@Override
public String toString() {
return "Project{" + "projectBean=" + projectBean + ", hasInited=" + hasInited + '}';
}
/***************************** 顶层 Project 功能接口:初始化、登陆、支付、退出 **********************************/
/**
* 初始化
*/
public void init(Activity activity, String gameid, String gamekey, CallBackListener callBackListener) {}
/**
* 登录
*/
public void login(Activity activity, HashMap loginParams) {}
/**
* 支付
*/
public void pay(Activity activity, HashMap payParams, CallBackListener callBackListener) {}
/**
* 切换账号
*/
public void switchAccount(Activity activity){}
/**
* 登出
*/
public void logout(Activity activity) {}
/**
* 退出
*/
public void exit(Activity activity, CallBackListener callBackListener) {}
/**
* 上报数据
*/
public void reportData(Context context, HashMap dataMap){}
/**
* 设置SDK账号监听
*/
public void setAccountCallBackLister(CallBackListener callBackLister){}
/**
* 显示SDK悬浮窗,将登录、支付等信息回调
*/
public void showFloatView(Activity activity){}
/**
* 关闭SDK悬浮窗
*/
public void dismissFloatView(Activity activity){}
/**
* 拓展接口,处理渠道的定制接口
*/
public void extendFunction(Activity activity, int functionType, Object object, CallBackListener callBackListener){}
/**
* 获取渠道ID
* @return
*/
public String getChannelID(){
return null;
}
/******************************* 顶层 Project 生命周期接口 (目前实现各插件的生命周期)******************************/
public void onCreate(Activity activity, Bundle savedInstanceState) {
PluginManager.getInstance().onCreate(activity, savedInstanceState);
}
public void onStart(Activity activity) {
PluginManager.getInstance().onStart(activity);
}
public void onResume(Activity activity) {
PluginManager.getInstance().onResume(activity);
}
public void onPause(Activity activity) {
PluginManager.getInstance().onPause(activity);
}
public void onStop(Activity activity) {
PluginManager.getInstance().onStop(activity);
}
public void onRestart(Activity activity) {
PluginManager.getInstance().onRestart(activity);
}
public void onDestroy(Activity activity) {
PluginManager.getInstance().onDestroy(activity);
}
public void onNewIntent(Activity activity, Intent intent) {
PluginManager.getInstance().onNewIntent(activity,intent);
}
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
PluginManager.getInstance().onActivityResult(activity, requestCode, requestCode, data);
}
public void onRequestPermissionsResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
PluginManager.getInstance().onRequestPermissionsResult(activity,requestCode,permissions,grantResults);
}
}
2、抽象的功能插件Plugin类,主要是面向第三方SDK设计的,预定义生命周期方法。具体功能实现接口根据功能设计。
public class Plugin implements LifeCycleInterface, ProguardInterface {
public PluginBeanList.PluginBean pluginBean;
private boolean hasInited;
protected synchronized void initPlugin() {
if (hasInited) {
return;
}
hasInited = true;
}
@Override
public String toString() {
return "Plugin{" + "pluginMessage=" + pluginBean + ", hasInited=" + hasInited + '}';
}
/****************************************生命周期方法*********************************************/
public void onCreate(Context context, Bundle savedInstanceState) {}
public void onStart(Context context) {}
public void onResume(Context context) {}
public void onPause(Context context) {}
public void onStop(Context context) {}
public void onRestart(Context context) {}
public void onDestroy(Context context) {}
public void onNewIntent(Context context, Intent intent){}
public void onActivityResult(Context context, int requestCode, int resultCode, Intent data) {}
public void onRequestPermissionsResult(Context context, int requestCode, String[] permissions, int[] grantResults) {}
}
3、抽象的渠道Channel类,主要是面向渠道SDK设计的,主要分为业务必须接口和非业务必须接口。必须业务接口设计为抽象,子类必须实现的。
/**
* 用于描述渠道SDK的顶层接口
*/
public abstract class Channel extends ChannelListenerImpl implements LifeCycleInterface, ProguardInterface {
/***************************** Channel 加载必须接口 **********************************/
/**
* 实例渠道插件对象,必须实现
*/
protected abstract void initChannel();
public ChannelBeanList.ChannelBean channelBean;
@Override
public String toString() {
return "Channel{" + "channelBean=" + channelBean +'}';
}
/****************************** 必须业务逻辑接口 ****************************/
public static final String PARAMS_OAUTH_TYPE = "PARAMS_OAUTH_TYPE";
public static final String PARAMS_OAUTH_URL = "PARAMS_OAUTH_URL";
/**
* 返回渠道的ID(用于识别渠道)
*/
public abstract String getChannelID();
/**
* 由于个别渠道只简单实现登录、支付接口,
* 对外提供该接口给CP判断该接口是否已实现
* @param FuncType
* @return
*/
public abstract boolean isSupport(int FuncType);
/**
* 渠道SDK初始化
*/
public abstract void init(Context context, HashMap initMap, CallBackListener initCallBackListener);
/**
* 渠道SDK登录
*/
public abstract void login(Context context, HashMap loginMap, CallBackListener loginCallBackListener);
/**
* 渠道切换账号
*/
public abstract void switchAccount(Context context, CallBackListener changeAccountCallBackLister);
/**
* 渠道SDK注销账号
*/
public abstract void logout(Context context, CallBackListener logoutCallBackLister);
/**
* 渠道SDK支付
*/
public abstract void pay(Context context, HashMap payMap, CallBackListener payCallBackListener);
/**
* 渠道SDK退出
*/
public abstract void exit(Context context, CallBackListener exitCallBackLister);
/****************************** 非必须业务逻辑接口 ****************************/
/**
* 返回渠道版本号
*/
public String getChannelVersion(){
return null;
}
/**
* 渠道SDK个人中心
*/
public void enterPlatform(Context context, CallBackListener enterPlatformCallBackLister){}
/**
* 显示渠道SDK悬浮窗
*/
public void showFloatView(Context context){}
/**
* 关闭渠道SDK悬浮窗
*/
public void dismissFloatView(Context context){}
/**
* 渠道SDK上报数据
*/
public void reportData(Context context, HashMap dataMap){}
/**
* 横竖屏
* @return true为横屏,false为竖屏
*/
public boolean getOrientation(Context context){
boolean isLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
return isLandscape;
}
/****************************** 非业务逻辑 生命周期接口 ****************************/
@Override
public void onCreate(Context context, Bundle savedInstanceState) {}
@Override
public void onStart(Context context) {}
@Override
public void onResume(Context context) {}
@Override
public void onPause(Context context) {}
@Override
public void onStop(Context context) {}
@Override
public void onRestart(Context context) {}
@Override
public void onDestroy(Context context) {}
@Override
public void onNewIntent(Context context, Intent intent) {}
@Override
public void onActivityResult(Context context, int requestCode, int resultCode, Intent data) {}
@Override
public void onRequestPermissionsResult(Context context, int requestCode, String[] permissions, int[] grantResults) {}
}
配置文件的bean类
配置文件的bean类,通过反射加载Project / Plugin / Channel具体实现对象。示例展示Project,其他两个是类似的,详看项目Demo。
public class ProjectBeanList extends ProguardObject{
private List project;//注意解析的名字要跟文件一致,不然会导致解析错误
public List getProject() {
return project;
}
public void setProject(List project) {
this.project = project;
}
public static class ProjectBean extends ProguardObject{
private static final String TAG = "ProjectBean";
/**
* 反射插件的单例模式方法
* 返回的插件可能为空
* @return
*/
public Project invokeGetInstance() {
Project p = null;
Class> glass = null;
if (TextUtils.isEmpty(class_name)) {
LogUtils.debug_w(TAG, "invokeGetInstance: the class_name is blank");
return p;
}
try {
glass = Class.forName(class_name);
} catch (ClassNotFoundException e) {
LogUtils.debug_w(TAG, "invokeGetInstance: " + "do not find " + class_name);
}
try {
//尝试调用getInstance
Method m = glass.getDeclaredMethod("getInstance", new Class>[]{});
m.setAccessible(true);
p = (Project) m.invoke(null, new Object[]{});
} catch (NoSuchMethodException e1) {
//调用getInstance失败后,尝试new其对象
try {
p = (Project) glass.newInstance();
} catch (Exception exception) {
LogUtils.debug_w(TAG, "glass.newInstance(): " + "do not find " + class_name);
}
} catch (Exception exception) {
LogUtils.debug_w(TAG, "glass.getInstance(): " + "do not find " + class_name);
}
if (p == null) {
LogUtils.debug_w(TAG, class_name + " is empty.");
} else {
p.projectBean = this;
}
return p;
}
/**
* project_name : 项目名称
* class_name : 项目入口类
* description : 项目描述
* version : 版本信息
*/
private String project_name;
private String class_name;
private String description;
private String version;
public String getProject_name() {
return project_name;
}
public void setProject_name(String plugin_name) {
this.project_name = plugin_name;
}
public String getClass_name() {
return class_name;
}
public void setClass_name(String class_name) {
this.class_name = class_name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
@Override
public String toString() {
return "ProjectBean{" +
" project_name='" + project_name + '\'' +
", class_name='" + class_name + '\'' +
", description='" + description + '\'' +
", version='" + version + '\'' +
'}';
}
}
}
配置管理类
主要加载配置文件及对外提供加载接口和实现部分逻辑控制。(示例展示Project,其他两个是类似的,详看项目Demo。注意:Plugin需遍历生命周期加载N个功能插件的生命周期。)
public class ProjectManager {
private static final String TAG = "ProjectManager";
private static String PROJECT_CONFIG = "Project_config.txt";
private static Project project;
private HashMap projectBeans = new HashMap<>();
/********************* 同步锁双重检测机制实现单例模式(懒加载)********************/
private volatile static ProjectManager projectManager;
public static ProjectManager init(Context context) {
if (projectManager == null) {
synchronized (ProjectManager.class) {
if (projectManager == null) {
projectManager = new ProjectManager(context);
}
}
}
return projectManager;
}
public static ProjectManager getInstance() {
return projectManager;
}
/********************* 同步锁双重检测机制实现单例模式 ********************/
private ProjectManager(Context context) {
parse(context, PROJECT_CONFIG);
}
private void parse(Context context, String pluginFilePath) {
//从配置文件中,读取插件配置
StringBuilder projectContent = FileUtils.readAssetsFile(context, pluginFilePath);
String strProjectContent = String.valueOf(projectContent);
//进行解析
Gson gson = new Gson();
if (!TextUtils.isEmpty(strProjectContent)) {
try {
ProjectBeanList projectBeanList = gson.fromJson(strProjectContent, ProjectBeanList.class);
if (projectBeanList.getProject() != null && projectBeanList.getProject().size() != 0) {
//如果解析结果无误,载入到listPluginBean中去
for (ProjectBeanList.ProjectBean projectBean : projectBeanList.getProject()) {
projectBeans.put(projectBean.getProject_name(), projectBean);
}
//打印解析结果
LogUtils.debug_i(TAG, PROJECT_CONFIG +" parse: \n" + projectBeans.toString());
} else {
//解析结果出错
LogUtils.e(TAG, PROJECT_CONFIG + " parse error.");
}
} catch (Exception e) {
//解析结果出错
LogUtils.e(TAG, PROJECT_CONFIG + " parse exception.");
e.printStackTrace();
}
}else {
LogUtils.e(TAG, PROJECT_CONFIG + " parse is blank.");
}
}
private boolean hasLoaded;
private static HashMap ProjectLists = new HashMap();
/**
* 加载所有的Project,可能存在多个项目
*/
public synchronized void loadAllProjects() {
if (hasLoaded) {
return;
}
HashMap entries = projectBeans;
Set set = entries.keySet();
for (String key : set) {
loadProject(key);
}
LogUtils.debug_i(TAG, "loadAllProjects:" + ProjectLists.toString());
hasLoaded = true;
}
/**
* 加载一个项目,返回的Project可能为空
*
* @param projectName
* @return
* @throws RuntimeException
*/
private Project loadProject(String projectName) throws RuntimeException {
// 1.查看从配置文件中读取的插件列表,是否存在此插件
HashMap entries = projectBeans;
ProjectBeanList.ProjectBean projectBean = entries.get(projectName);
if (projectBean == null) {
LogUtils.debug_i(TAG, "The project [" + projectName + "] does not exists in " + PROJECT_CONFIG);
return null;
}
Project project = null;
// 2.调用其单例模式方法
project = projectBean.invokeGetInstance();
if (project != null) {
// 3.反射初始化插件
project.initProject();
// 4.将已加载好的插件,添加到插件列表中去
ProjectLists.put(projectName, project);
}
return project;
}
/**
* 获取特定项目
* 可能为空
*
* @param projectName
* @return
*/
public Project getProject(String projectName) {
if (!hasLoaded) {
LogUtils.debug_i(TAG, "getProject: " + projectName + "Project not loaded yet");
return null;
}
Project project = null;
HashMap entries = ProjectLists;
project = entries.get(projectName);
return project;
}
}
项目回调设计
任何一个项目都会存在模块之间的调用,任何事件的事件流都会有事件结果回调。都会涉及到回调机制。
相关原理可参考下这篇文章: Java回调机制解读简单介绍下项目的回调设计思路:
定义基类的回调接口,定义成功和失败回调,失败回调可以通过错误码和描述信息来区分错误事件的详细信息。这里有个小技巧就是定义返回错误码,CP就可以根据错误码做相关的界面UI提示,这里就可以偷懒尽量避免做多语言和多UI处理啦。
public interface CallBackListener {
/**
* 成功回调
* @param t 详细信息
*/
void onSuccess(T t);
/**
* 失败回调
*
* @param code 错误码
* @param msg 错误详细描述信息
*/
void onFailure(int code, String msg);
}
项目数据设计
一般项目都会涉及到数据的交互与数据存储,数据存储一般常用的数据存储方式SharedPreferences存储、文件存储、SQLite数据库存储、ContentProvider存储、网络存储等混合使用。相关的存储方式及使用这里就不介绍了。由于游戏SDK涉及到的数据量不大,主要以内存存储和SharedPreferences存储和文件存储为主。
PS:如果数据设计到多进程数据交互,建议使用ContentProvider存储方式。简单介绍下内存缓存的思路:
定义全局的集合,通过单例来管理相关的数据存储和读取接口。
public class BaseCache {
private Application mAppContext;
public Context getApplication() {
return mAppContext;
}
/********************* 同步锁双重检测机制实现单例模式(懒加载)********************/
private volatile static BaseCache sCache;
private BaseCache(Application appContext) {
mAppContext = appContext;
}
public static BaseCache getInstance() {
if (sCache == null) {
throw new RuntimeException("get(Context) never called");
}
return sCache;
}
public static BaseCache init(Application cxt) {
if (sCache == null) {
synchronized (BaseCache.class) {
if (sCache == null) {
sCache = new BaseCache(cxt);
}
}
}
return sCache;
}
/********************* 同步锁双重检测机制实现单例模式(懒加载)********************/
/**
* hashMap是线程不安全的,做全局缓存时,用锁来保证存储值
*/
private HashMap mConfigs = new HashMap<>();
private ReentrantLock mLock = new ReentrantLock();
public void put(String key, Object value){
mLock.lock();
mConfigs.put(key,value);
mLock.unlock();
}
public Object get(String key){
mLock.lock();
Object object = mConfigs.get(key);
mLock.unlock();
return object;
}
项目域名设计
因为SDK涉及到多个项目,每个项目肯定是会有不同的域名的,并且同一个项目也存在测试环境、沙盒环境、正式环境的域名区分,域名设计的主要的作用是统一管理域名及方便后续不同的项目来回切换不同的域名地址,这是比较重要的。
简单说下域名的设计思路:
一般修改域名不涉及到代码的修改,对外的包如何动态的修改域名设置呢?
1、通过后台配置预设域名,但是后台的设置会影响到线上环境。慎重。
2、通过包体的配置文件来读取域名,开发人员可以通过反编译包体修改域名调试代码。
PS:开发人员根据需求设计,但是切记要统一管理,方便维护。
public class UrlConfig {
private static final String TAG = "UrlConfig";
private static String Project_SDKUrl; //url
//项目的基础ip域名地址
private static String Project_BaseApi = "https://www.baidu.com/";
public static String getSdkUrl(){
return Project_SDKUrl;
}
public static void initUrl(){
//通过配置读取域名
String SDK_Base_Url = BaseCache.getInstance().getSdkUrl();
if (!TextUtils.isEmpty(SDK_Base_Url)){//通过配置文件来修改域名
Project_BaseApi = SDK_Base_Url;
}
LogUtils.debug_d(TAG,Project_SDKUrl);
}
/**
* 预设设置修改当前网络的请求域名接口
* 思考特殊的场景:
* 多个项目交叉调用时,域名地址不一样,可通过修改临时域名做请求。
*/
public static String getReSetUrl(String sdk_type, String channelId){
String tempUrl = "";
return tempUrl; //返回临时的域名
}
}
项目混淆设计
SDK是对外提供功能的,而且从安全性考虑的话,对外提供的包体都应该是经常各种混淆和加密处理的。不然被攻击的可能性就很高了。
项目的设计是定义顶层的类或接口,具体的实现类继承该类或者实现该接口就不混淆了。关于相关的混淆规则及配置可参考: Android混淆打包那些事儿
/**
* 定义基础混淆接口
*/
public interface ProguardInterface {
}
/**
* 定义基础混淆对象
*/
public class ProguardObject {
}
项目三方库的封装设计
在实际开发中,会使用到大量的三方库,避免重复造轮子,特别是网络库、图片加载图等。但是,开源库过几年就可能会有更好的开源库,或者原有的开源库有局限性满足不了现有的项目需求,需要拓展或者替换的新的框架。所以需要做封装处理,避免后续替换改动太大。可参考:
对于有多种可替代解决方案的业务逻辑,提供一种快速替换方法PS:项目中封装的是Volley,根据实际项目做了简单网络库的封装,不做任何商业模块使用,慎用!
public class RequestExecutor {
public static final String GET = "GET";
public static final String POST = "POST";
private IRequestManager iRequestManager;
private String method;
private String url;
private String header;
private String userAgent;
private Map params;
private RequestCallback requestCallback;
private RequestExecutor(RequestExecutor.Builder builder){
this.method = builder.method;
this.url = builder.url;
this.header = builder.header;
this.userAgent = builder.userAgent;
this.params = builder.params;
this.requestCallback = builder.requestCallback;
}
public void startRequest(){
/**
* 网络框架要替换成别的时候,实现具体封装就OK了,并修改具体实现
* 比如换成okhttp写法 :return new OkHttpRequestManager();
*/
iRequestManager = new VolleyRequestManager();
iRequestManager.setHeader(header);
iRequestManager.setUserAgent(userAgent);
if (RequestExecutor.GET.equals(method)){
iRequestManager.get(url,params,requestCallback);
}else if (RequestExecutor.POST.equals(method)){
iRequestManager.post(url,params,requestCallback);
}
}
/**
* 取消当前的网络请求,
*/
public void cancel(){
iRequestManager.cancel();
}
public static class Builder{
private String method;
private String url;
private String header;
private String userAgent;
private Map params;
private RequestCallback requestCallback;
public RequestExecutor build(){
return new RequestExecutor(this);
}
public RequestExecutor.Builder setMethod(String method){
this.method = method;
return this;
}
public RequestExecutor.Builder setUrl(String url){
this.url = url;
return this;
}
public RequestExecutor.Builder setHeader(String header){
this.header = header;
return this;
}
public RequestExecutor.Builder setUserAgent(String userAgent){
this.userAgent = userAgent;
return this;
}
public RequestExecutor.Builder setParams(HashMap params){
this.params = params;
return this;
}
public RequestExecutor.Builder setRequestCallback(RequestCallback requestCallback){
this.requestCallback = requestCallback;
return this;
}
}
}
结语:
由于篇幅原因,第二部分请看下一篇:
手游SDK —第四篇(SDK架构代码实现篇(下)- 项目需求开发)
如果觉得我的文章对你有帮助,请随意赞赏。您的支持将鼓励我继续创作!