游戏SDK开发笔录
来首歌
华年——鹿先森乐队
在最好的年纪做最好的事!
开发环境:
- Android studio 3.0
- win10
- jdk 1.8
文章概述
本文是主要介绍游戏sdk开发的一些经验,主要为游戏提供:登录、登出、注册、修改密码、支付等接口(有些游戏sdk会有游戏角色的统计、升级的统计),也会在sdk里做一些公司需要的统计、用户社区、广告等功能。
考虑因素
- sdk是提供给游戏接入的,所以游戏调用sdk的一些功能时,需要把cp(sdk接入方)需要的数据,通过sdk在返回给cp。cp可根据返回结果,在进行下一步的操作。
- 通过怎么的形式返回给cp,让cp根据数据结果进行下一步操作。这里个人给出两种方案,一种是广播、一种是自定义接口回调。
- cp在接入时碰到问题,如何能和cp接入人员进行很好的联调。
注意事项
1.适配eclipse接入
2.sdk开发,在读取资源时,一定要动态去读取,不要R.layout 或者R.id 等这样写。因为大多游戏接入时的工具都是借助于eclipse的。这样写eclispe引用jar时会找不到资源,当然android studio生成arr包是没问题的。
文章末尾会把动态读取资源的工具类贴出来。
准备工作
创建一个项目,然后在创建一个modle,把这个modle作为一个类库直接进行开发。app依赖就可以了,这样直接就可以边开发sdk边调试了。
开发
结合考虑因素3,可以把cp接入时产生的崩溃信息进行本地存储,或者直接进行上传我方服务器。这里是做了本地存储,调试时cp可直接把文件发过来,帮他们定位问题所在。(因为cp接入人员很多都不是android人员,所以如果进行服务器上传,会比较浪费资源)
崩溃日志信息本地存储
主要借助于UncaughtExceptionHandler,来进行异常捕获。可以把文件存到本地,也可以设置保存多少天自动删除。贴出详细代码,可以看注释介绍:
public class CrashHandler implements Thread.UncaughtExceptionHandler { public static String TAG = "JQCrrash"; // 系统默认的UncaughtException处理类 private Thread.UncaughtExceptionHandler mDefaultHandler; private static CrashHandler instance = new CrashHandler(); private Context mContext; // 用来存储设备信息和异常信息 private Map<String, String> infos = new HashMap<String, String>(); // 用于格式化日期,作为日志文件名的一部分 private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); /** 保证只有一个CrashHandler实例 */ private CrashHandler() { } /** 获取CrashHandler实例 ,单例模式 */ public static CrashHandler getInstance() { return instance; } /** * 初始化 * * @param context */ public void init(Context context) { mContext = context; // 获取系统默认的UncaughtException处理器 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); // 设置该CrashHandler为程序的默认处理器 Thread.setDefaultUncaughtExceptionHandler(this); autoClear(5); } /** * 当UncaughtException发生时会转入该函数来处理 */ @Override public void uncaughtException(Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null) { // 如果用户没有处理则让系统默认的异常处理器来处理 mDefaultHandler.uncaughtException(thread, ex); } else { SystemClock.sleep(3000); // 退出程序 android.os.Process.killProcess(android.os.Process.myPid()); System.exit(1); } } /** * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成. * @param ex * @return true:如果处理了该异常信息; 否则返回false. */ private boolean handleException(Throwable ex) { if (ex == null) return false; try { // 使用Toast来显示异常信息 new Thread() { @Override public void run() { Looper.prepare(); Toast.makeText(mContext, "很抱歉,程序出现异常,即将重启.", Toast.LENGTH_LONG).show(); Looper.loop(); } }.start(); // 收集设备参数信息 collectDeviceInfo(mContext); // 保存日志文件 saveCrashInfoFile(ex); SystemClock.sleep(3000); } catch (Exception e) { e.printStackTrace(); } return true; } /** * 收集设备参数信息 * * @param ctx */ public void collectDeviceInfo(Context ctx) { try { PackageManager pm = ctx.getPackageManager(); PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES); if (pi != null) { String versionName = pi.versionName + ""; String versionCode = pi.versionCode + ""; infos.put("versionName", versionName); infos.put("versionCode", versionCode); } } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "an error occured when collect package info", e); } Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); infos.put(field.getName(), field.get(null).toString()); } catch (Exception e) { Log.e(TAG, "an error occured when collect crash info", e); } } } /** * 保存错误信息到文件中 * @param ex * @return 返回文件名称,便于将文件传送到服务器 * @throws Exception */ private String saveCrashInfoFile(Throwable ex) throws Exception { StringBuffer sb = new StringBuffer(); try { SimpleDateFormat sDateFormat = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); String date = sDateFormat.format(new java.util.Date()); sb.append("\r\n" + date + "\n"); for (Map.Entry<String, String> entry : infos.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); sb.append(key + "=" + value + "\n"); } Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); ex.printStackTrace(printWriter); Throwable cause = ex.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.flush(); printWriter.close(); String result = writer.toString(); sb.append(result); String fileName = writeFile(sb.toString()); return fileName; } catch (Exception e) { Log.e(TAG, "an error occured while writing file...", e); sb.append("an error occured while writing file...\r\n"); writeFile(sb.toString()); } return null; } private String writeFile(String sb) throws Exception { String time = formatter.format(new Date()); String fileName = "crash-" + time + ".log"; if (FileUtil.hasSdcard()) { String path = getGlobalpath(); File dir = new File(path); if (!dir.exists()) dir.mkdirs(); FileOutputStream fos = new FileOutputStream(path + fileName, true); fos.write(sb.getBytes()); fos.flush(); fos.close(); } return fileName; } public static String getGlobalpath() { return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "crash" + File.separator; } public static void setTag(String tag) { TAG = tag; } /** * 文件删除 * @param autoClearDay 文件保存天数 */ public void autoClear(final int autoClearDay) { FileUtil.delete(getGlobalpath(), new FilenameFilter() { @Override public boolean accept(File file, String filename) { String s = FileUtil.getFileNameWithoutExtension(filename); int day = autoClearDay < 0 ? autoClearDay : -1 * autoClearDay; String date = "crash-" + DateUtil.getOtherDay(day); return date.compareTo(s) >= 0; } }); } }
只需要在Application的onCreat方法里调用即可:
CrashHandler.getInstance().init(context);
数据回调cp方式
结合考虑因素2,回调方式分为两种:
- 广播形式回传
使用形式:
//cp 重写广播后,在onCreat方法里注册即可 JQSDKReceiver jqsdkReceiver= new JQSDKReceiver(new JQSDKReceiverCallback() { @Override public void loginCallback(String result) { } @Override public void exitCallback(String result) { } @Override public void initCallback(Boolean result) { } }); //注册
IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("JQSDK"); registerReceiver(jqsdkReceiver,intentFilter);
Sdk编写方式:
a、重写广播onReceive方法
public class JQSDKReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { } }
b、自定义一个接口,声明广播构造方法时,声明自定义的接口。
// 广播在接收到数据后,可以把数据传给接口,cp在重写时即可在接口回调里获取数据 private JQSDKReceiverCallback callback; public JQSDKReceiver(JQSDKReceiverCallback callback){ this.callback=callback; } // 在onReceive里拿到数据后调用,既可以把数据回传给接口 private void initCallback(Boolean result){ callback.initCallback(result); }
用广播就要考虑到一点,多个接口都需要回传数据给cp,难道要创建多个广播?当然不是。
用个比较笨的方法 1.声明一个全局的变量 flag,默认值是0 2.不同接口会要回传数据时设置个不同的flag值 3.onReceive接收时,根据flag值来区分是哪个接口发送的广播,然后取出对应的数据,传到接口中即可,最后一定要把flag值置为默认值,也要把这个广播移除 if (Params.FLAG == 1) { Params.FLAG=0; String string = intent.getStringExtra(Params.JQSDK_LOGIN_RESULT); loginCallback(string);//把数据传给接口 intent.removeExtra(Params.JQSDK_LOGIN_RESULT); }else if (Params.FLAG==2){ Params.FLAG=0; String string = intent.getStringExtra(Params.JQSDK_EXIT_RESULT); exitCallback(string); intent.removeExtra(Params.JQSDK_EXIT_RESULT); } 到这里其实已经完成游戏sdk比较关键的地方了,其他业务逻辑你就可以在sdk去尽情的编写了。
自定义接口回传方式(推荐使用这种方式)
使用方式:
//这种方式比起广播有很大的优势:1.广播有一个时间响应接收。 2.这种方式对于非andorid人员去接入时,更能容易的接入。调用后可直接在回调里拿数据。层次感更清楚 JQInitInterface.getInstance().init(this, new JQGameInitCallback() { @Override public void initCallBack(String s) { if (s.equals("1")){ Toast.makeText(Main2Activity.this,"初始化成功",Toast.LENGTH_SHORT).show(); } } });
Sdk编写方式:
//定义回传数据的接口 public interface JQGameInitCallback { void initCallBack(String s); } private JQGameInitCallback callback; /** * @return 初始化sdk操作 * */ public void init(final Activity activity, final JQGameInitCallback callback){ //声明这个回传数据的接口 this.callback=callback; //执行sdk的一些初始化操作,初始化完成过后,通过callback回调给接口 } //当cp接入时,调用这个init方法就能直接在JQGameInitCallback 里拿到初始化后的结果了
上面是无界面的回调,有人会问,有界面怎么办,其实道理也是相同的。
游戏sdk的登陆界面可以dialog或者Activity(设置成dialog形式)。
下面以Dialog为例子:
public class JobDialog extends DialogFragment{ // dialog内部声明一个静态的方法用来获取dialog的实例。 对外暴露 public static JobDialog newInstance( String code,JobChooseIntetface jobIntetface) { LoginDialog fragment = new LoginDialog (); Bundle bundle = new Bundle(); bundle.putString("job", code); fragment.setArguments(bundle); jobChooseIntetface=jobIntetface; return fragment; } //内部定义接口,重新写个类定义都可 public interface JobChooseIntetface { void onItemLeftClick(int id, String profession); } private static JobChooseIntetface jobChooseIntetface;}
//外部调用
JobDialog jobDialog = JobDialog.newInstance(jobData, new JobDialog.JobChooseIntetface() { @Override public void onItemLeftClick(int id, String profession) { //这里就可以拿到 接口回传的数据 } }); //这个show方法你到newInstance内部调用也可以 jobDialog.show(getSupportFragmentManager(), "jobDialog");
动态获取资源工具类
public class ResourceUtil { public static int getLayoutId(Context paramContext, String paramString) { return paramContext.getResources().getIdentifier(paramString, "layout", paramContext.getPackageName()); } public static int getStringId(Context paramContext, String paramString) { return paramContext.getResources().getIdentifier(paramString, "string", paramContext.getPackageName()); } public static int getDrawableId(Context paramContext, String paramString) { return paramContext.getResources().getIdentifier(paramString, "drawable", paramContext.getPackageName()); } public static int getStyleId(Context paramContext, String paramString) { return paramContext.getResources().getIdentifier(paramString, "style", paramContext.getPackageName()); } public static int getId(Context paramContext, String paramString) { return paramContext.getResources().getIdentifier(paramString, "id", paramContext.getPackageName()); } public static int getColorId(Context paramContext, String paramString) { return paramContext.getResources().getIdentifier(paramString, "color", paramContext.getPackageName()); } public static int getAnimId(Context paramContext, String paramString) { return paramContext.getResources().getIdentifier(paramString, "anim", paramContext.getPackageName()); }
使用方式:
setContentView(ResourceUtil.getLayoutId(this,"activity_new_user_center"));
文章主要介绍,有关游戏sdk开发的数据回传方式、联调方式、资源的获取方式。
注:
1.使用android studio开发完后,生成arr包,解压后取出各个资源文件和jar,eclipse就可以直接依赖使用了。
2.有关sdk的混淆要做好,哪些可以混淆哪些不能混淆。