由于我們寫的代碼難免會(huì)出現(xiàn)一些bug眶拉,以及由于測試環(huán)境和生產(chǎn)環(huán)境差異導(dǎo)致出現(xiàn)的一些異常千埃,在測試過程中沒有發(fā)現(xiàn),而在app上線之后會(huì)偶然出現(xiàn)的一些bug忆植,以至于app在使用過程中出現(xiàn)ANR放可,這是個(gè)令人蛋疼的現(xiàn)象,app卡死朝刊、出現(xiàn)黑屏等耀里,總之當(dāng)app出現(xiàn)異常時(shí)的用戶體驗(yàn)不友好,我們開發(fā)者需要去捕獲這些異常拾氓,收集這些異常信息冯挎,并且上傳到服務(wù)器,利于開發(fā)人員去解決這些問題咙鞍,同時(shí)房官,我們還需要給用戶一個(gè)友好的交互體驗(yàn)。
我也查找了許多捕獲崩潰異常的文章來看续滋,但大多都千篇一律翰守,只說了怎么捕獲異常,處理異常疲酌,并沒有對捕獲異常后的界面交互做出很好的處理蜡峰,系統(tǒng)出現(xiàn)ANR時(shí)會(huì)先卡一段時(shí)間(這個(gè)時(shí)間還有點(diǎn)長)然后彈出一個(gè)系統(tǒng)默認(rèn)的對話框,但是我現(xiàn)在需要的是當(dāng)出現(xiàn)異常情況時(shí),立馬彈出對話框事示,并且對話框我想自定義界面早像。
最終實(shí)現(xiàn)的效果如圖:
<img src="http://upload-images.jianshu.io/upload_images/1159224-cdf0b4169183ecaf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" width="40%" alt="最終效果圖" align="center" />
首先需要自定義Application
僻肖,并且在AndroidManifest.xml
中進(jìn)行配置
還需要自定義一個(gè)應(yīng)用異常捕獲類AppUncaughtExceptionHandler
肖爵,它必須得實(shí)現(xiàn)Thread.UncaughtExceptionHandler
接口,另外還需要重寫uncaughtException
方法臀脏,去按我們自己的方式來處理異常
在Application
中我們只需要初始化自定義的異常捕獲類即可:
@Override public void onCreate() {
super.onCreate();
mInstance = this;
// 初始化文件目錄
SdcardConfig.getInstance().initSdcard();
// 捕捉異常
AppUncaughtExceptionHandler crashHandler = AppUncaughtExceptionHandler.getInstance();
crashHandler.init(getApplicationContext());
}
其中文件目錄是異常信息保存在sd卡中的目錄劝堪,還有我們是整個(gè)app中全局捕獲異常,所以我們自定義的捕獲類是單例揉稚。
/**
* 初始化捕獲類
*
* @param context
*/
public void init(Context context) {
applicationContext = context.getApplicationContext();
crashing = false;
//獲取系統(tǒng)默認(rèn)的UncaughtException處理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//設(shè)置該CrashHandler為程序的默認(rèn)處理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
完成以上過程后秒啦,接著需要重寫uncaughtException
方法:
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (crashing) {
return;
}
crashing = true;
// 打印異常信息
ex.printStackTrace();
// 我們沒有處理異常 并且默認(rèn)異常處理不為空 則交給系統(tǒng)處理
if (!handlelException(ex) && mDefaultHandler != null) {
// 系統(tǒng)處理
mDefaultHandler.uncaughtException(thread, ex);
}
byebye();
}
private void byebye() {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
既然是我們自己處理異常,所以會(huì)先執(zhí)行handlelException(ex)
方法:
private boolean handlelException(Throwable ex) {
if (ex == null) {
return false;
}
try {
// 異常信息
String crashReport = getCrashReport(ex);
// TODO: 上傳日志到服務(wù)器
// 保存到sd卡
saveExceptionToSdcard(crashReport);
// 提示對話框
showPatchDialog();
} catch (Exception e) {
return false;
}
return true;
}
獲取的異常信息包括系統(tǒng)信息搀玖,app版本信息余境,以及手機(jī)制造商信息等:
/**
* 獲取異常信息
* @param ex
* @return
*/
private String getCrashReport(Throwable ex) {
StringBuffer exceptionStr = new StringBuffer();
PackageInfo pinfo = CrashApplication.getInstance().getLocalPackageInfo();
if (pinfo != null) {
if (ex != null) {
//app版本信息
exceptionStr.append("App Version:" + pinfo.versionName);
exceptionStr.append("_" + pinfo.versionCode + "\n");
//手機(jī)系統(tǒng)信息
exceptionStr.append("OS Version:" + Build.VERSION.RELEASE);
exceptionStr.append("_");
exceptionStr.append(Build.VERSION.SDK_INT + "\n");
//手機(jī)制造商
exceptionStr.append("Vendor: " + Build.MANUFACTURER+ "\n");
//手機(jī)型號
exceptionStr.append("Model: " + Build.MODEL+ "\n");
String errorStr = ex.getLocalizedMessage();
if (TextUtils.isEmpty(errorStr)) {
errorStr = ex.getMessage();
}
if (TextUtils.isEmpty(errorStr)) {
errorStr = ex.toString();
}
exceptionStr.append("Exception: " + errorStr + "\n");
StackTraceElement[] elements = ex.getStackTrace();
if (elements != null) {
for (int i = 0; i < elements.length; i++) {
exceptionStr.append(elements[i].toString() + "\n");
}
}
} else {
exceptionStr.append("no exception. Throwable is null\n");
}
return exceptionStr.toString();
} else {
return "";
}
}
將異常信息保存到sd卡這個(gè)我覺得可選吧,但是上傳到服務(wù)端還是很有必要的:
/**
* 保存錯(cuò)誤報(bào)告到sd卡
* @param errorReason
*/
private void saveExceptionToSdcard(String errorReason) {
try {
Log.e("CrashDemo", "AppUncaughtExceptionHandler執(zhí)行了一次");
String time = mFormatter.format(new Date());
String fileName = "Crash-" + time + ".log";
if (SdcardConfig.getInstance().hasSDCard()) {
String path = SdcardConfig.LOG_FOLDER;
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(path + fileName);
fos.write(errorReason.getBytes());
fos.close();
}
} catch (Exception e) {
Log.e("CrashDemo", "an error occured while writing file..." + e.getMessage());
}
}
保存在sd卡中的異常文件格式:
至于上傳到服務(wù)器灌诅,就比較靈活了芳来,可以將整個(gè)文件上傳,或者上傳異常信息的字符串猜拾,可以和后端開發(fā)人員配合即舌。
因?yàn)椴东@異常后我要馬上關(guān)閉掉app即上面的byebye
方法,是將app整個(gè)進(jìn)程殺死挎袜,如果接著要顯示提示對話框顽聂,則需要在新的任務(wù)棧中打開activity
:
public static Intent newIntent(Context context, String title, String ultimateMessage) {
Intent intent = new Intent();
intent.setClass(context, PatchDialogActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(EXTRA_TITLE, title);
intent.putExtra(EXTRA_ULTIMATE_MESSAGE, ultimateMessage);
return intent;
}
對話框中給出了重啟操作的選項(xiàng),重啟過程的實(shí)現(xiàn):
private void restart() {
Intent intent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
super.onDestroy();
}
源代碼地址:https://github.com/shenhuniurou/BlogDemos/tree/master/CrashDemo歡迎star盯仪。