遊戲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的混淆要做好,哪些可以混淆哪些不能混淆。














發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章