由于我們寫的代碼難免會出現(xiàn)一些bug,以及由于測試環(huán)境和生產環(huán)境差異導致出現(xiàn)的一些異常,在測試過程中沒有發(fā)現(xiàn),而在app上線之后會偶然出現(xiàn)的一些bug,以至于app在使用過程中出現(xiàn)ANR,這是個令人蛋疼的現(xiàn)象,app卡死、出現(xiàn)黑屏等,總之當app出現(xiàn)異常時的用戶體驗不友好,我們開發(fā)者需要去捕獲這些異常,收集這些異常信息,并且上傳到服務器,利于開發(fā)人員去解決這些問題,同時,我們還需要給用戶一個友好的交互體驗。
我也查找了許多捕獲崩潰異常的文章來看,但大多都千篇一律,只說了怎么捕獲異常,處理異常,并沒有對捕獲異常后的界面交互做出很好的處理,系統(tǒng)出現(xiàn)ANR時會先卡一段時間(這個時間還有點長)然后彈出一個系統(tǒng)默認的對話框,但是我現(xiàn)在需要的是當出現(xiàn)異常情況時,立馬彈出對話框,并且對話框我想自定義界面。
最終實現(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中進行配置

還需要自定義一個應用異常捕獲類AppUncaughtExceptionHandler,它必須得實現(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卡中的目錄,還有我們是整個app中全局捕獲異常,所以我們自定義的捕獲類是單例。
/**
* 初始化捕獲類
*
* @param context
*/
public void init(Context context) {
applicationContext = context.getApplicationContext();
crashing = false;
//獲取系統(tǒng)默認的UncaughtException處理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//設置該CrashHandler為程序的默認處理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
完成以上過程后,接著需要重寫uncaughtException方法:
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (crashing) {
return;
}
crashing = true;
// 打印異常信息
ex.printStackTrace();
// 我們沒有處理異常 并且默認異常處理不為空 則交給系統(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);
}
既然是我們自己處理異常,所以會先執(zhí)行handlelException(ex)方法:
private boolean handlelException(Throwable ex) {
if (ex == null) {
return false;
}
try {
// 異常信息
String crashReport = getCrashReport(ex);
// TODO: 上傳日志到服務器
// 保存到sd卡
saveExceptionToSdcard(crashReport);
// 提示對話框
showPatchDialog();
} catch (Exception e) {
return false;
}
return true;
}
獲取的異常信息包括系統(tǒng)信息,app版本信息,以及手機制造商信息等:
/**
* 獲取異常信息
* @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");
//手機系統(tǒng)信息
exceptionStr.append("OS Version:" + Build.VERSION.RELEASE);
exceptionStr.append("_");
exceptionStr.append(Build.VERSION.SDK_INT + "\n");
//手機制造商
exceptionStr.append("Vendor: " + Build.MANUFACTURER+ "\n");
//手機型號
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卡這個我覺得可選吧,但是上傳到服務端還是很有必要的:
/**
* 保存錯誤報告到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卡中的異常文件格式:

至于上傳到服務器,就比較靈活了,可以將整個文件上傳,或者上傳異常信息的字符串,可以和后端開發(fā)人員配合。
因為捕獲異常后我要馬上關閉掉app即上面的byebye方法,是將app整個進程殺死,如果接著要顯示提示對話框,則需要在新的任務棧中打開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à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。