來首歌
華年——鹿先森樂隊
在最好的年紀做最好的事!
開發環境:
- 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的混淆要做好,哪些可以混淆哪些不能混淆。