游戏SDK开发笔录

来首歌

华年——鹿先森乐队

在最好的年纪做最好的事!

 

 

 

开发环境:

  1. Android studio 3.0
  2. win10
  3. jdk 1.8

文章概述

本文是主要介绍游戏sdk开发的一些经验,主要为游戏提供:登录、登出、注册、修改密码、支付等接口(有些游戏sdk会有游戏角色的统计、升级的统计),也会在sdk里做一些公司需要的统计、用户社区、广告等功能。

考虑因素

  1. sdk是提供给游戏接入的,所以游戏调用sdk的一些功能时,需要把cp(sdk接入方)需要的数据,通过sdk在返回给cp。cp可根据返回结果,在进行下一步的操作。
  2. 通过怎么的形式返回给cp,让cp根据数据结果进行下一步操作。这里个人给出两种方案,一种是广播、一种是自定义接口回调。
  3. 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的混淆要做好,哪些可以混淆哪些不能混淆。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值