目錄
認(rèn)識(shí)與作用
Crash的捕獲
Crash信息的獲取
Crash日志寫入上傳
使用方式
一些注意
最后
認(rèn)識(shí)與作用
CrashHandler: 崩潰處理器盒齿,捕獲Crash信息并作出相應(yīng)的處理
測(cè)試使用:應(yīng)用在日常的開發(fā)中边翁,我們經(jīng)常需要去Logcat測(cè)試我們的App符匾,但由于很多原因啊胶,Android Monitor會(huì)閃屏或者Crash信息丟失。 這個(gè)時(shí)候就需要一個(gè)CrashHandler來將Crash寫入到本地方便我們隨時(shí)隨地查看。
上線使用:應(yīng)用的崩潰率是用戶衡量篩選應(yīng)用的重要標(biāo)準(zhǔn)琳彩,那么應(yīng)用上線以后 我們無法向用戶借手機(jī)來分析崩潰原因露乏。為了減低崩潰率瘟仿,這個(gè)時(shí)候需要CrashHandler來幫我們將崩潰信息返回給后臺(tái)劳较,以便及時(shí)修復(fù)观蜗。
下面我們就手把手寫一個(gè)實(shí)用、本地化抖仅、輕量級(jí)的CrashHandler吧撤卢。
Crash的捕獲
實(shí)現(xiàn)Thread.UncaughtExceptionHandler接口放吩,并重寫uncaughtException方法屎慢,此時(shí)你的CrashHandler就具備了接收處理異常的能力了腻惠。
調(diào)用Thread.setDefaultUncaughtExceptionHandler(CrashHandler),來使用我們自定義的CrashHandler來取代系統(tǒng)默認(rèn)的CrashHandler
結(jié)合單例模式
總體三步: 捕獲異常集灌、信息數(shù)據(jù)獲取欣喧、數(shù)據(jù)寫入和上傳
總體的初始化代碼如下:
private RCrashHandler(StringdirPath) {? ? ? ? mDirPath = dirPath;? ? ? ? File mDirectory =newFile(mDirPath);if(!mDirectory.exists()) {? ? ? ? ? ? mDirectory.mkdirs();? ? ? ? }? ? }? ? publicstaticRCrashHandler getInstance(StringdirPath) {if(INSTANCE ==null) {? ? ? ? ? ? synchronized (RCrashHandler.class) {if(INSTANCE ==null) {? ? ? ? ? ? ? ? ? ? INSTANCE =newRCrashHandler(dirPath);? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }returnINSTANCE;? ? }
/**
* 初始化
*
* @param context? ? ? 上下文
* @param crashUploader 崩潰信息上傳接口回調(diào)
*/publicvoidinit(Context context, CrashUploader crashUploader) {? ? ? ? mCrashUploader = crashUploader;? ? ? ? mContext = context;//保存一份系統(tǒng)默認(rèn)的CrashHandlermDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();//使用我們自定義的異常處理器替換程序默認(rèn)的Thread.setDefaultUncaughtExceptionHandler(this);? ? }/**
* 這個(gè)是最關(guān)鍵的函數(shù)唆阿,當(dāng)程序中有未被捕獲的異常驯鳖,系統(tǒng)將會(huì)自動(dòng)調(diào)用uncaughtException方法
*
* @param t 出現(xiàn)未捕獲異常的線程
* @param e 未捕獲的異常浅辙,有了這個(gè)ex,我們就可以得到異常信息
*/@Override? ? publicvoiduncaughtException(Thread t, Throwable e) {if(!catchCrashException(e) && mDefaultHandler !=null) {//沒有自定義的CrashHandler的時(shí)候就調(diào)用系統(tǒng)默認(rèn)的異常處理方式mDefaultHandler.uncaughtException(t, e);? ? ? ? }else{//退出應(yīng)用killProcess();? ? ? ? }? ? }/**
* 自定義錯(cuò)誤處理,收集錯(cuò)誤信息 發(fā)送錯(cuò)誤報(bào)告等操作均在此完成.
*
* @param ex
* @return true:如果處理了該異常信息;否則返回false.
*/private boolean catchCrashException(Throwable ex) {if(ex ==null) {returnfalse;? ? ? ? }newThread() {? ? ? ? ? ? publicvoidrun() {//? ? ? ? ? ? ? ? Looper.prepare();//? ? ? ? ? ? ? ? Toast.makeText(mContext, "很抱歉,程序出現(xiàn)異常,即將退出", 0).show();//? ? ? ? ? ? ? ? Looper.loop();Intent intent =newIntent();? ? ? ? ? ? ? ? intent.setClass(mContext, CrashActivity.class);? ? ? ? ? ? ? ? intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);? ? ? ? ? ? ? ? ActivityCollector.finishAll();? ? ? ? ? ? ? ? mContext.startActivity(intent);? ? ? ? ? ? }? ? ? ? }.start();//收集設(shè)備參數(shù)信息collectInfos(mContext, ex);//保存日志文件saveCrashInfo2File();//上傳崩潰信息uploadCrashMessage(infos);returntrue;? ? }/**
* 退出應(yīng)用
*/publicstaticvoidkillProcess() {//結(jié)束應(yīng)用newThread(newRunnable() {? ? ? ? ? ? @Override? ? ? ? ? ? publicvoidrun() {? ? ? ? ? ? ? ? Looper.prepare();? ? ? ? ? ? ? ? ToastUtils.showLong("哎呀,程序發(fā)生異常啦...");? ? ? ? ? ? ? ? Looper.loop();? ? ? ? ? ? }? ? ? ? }).start();try{? ? ? ? ? ? Thread.sleep(2000);? ? ? ? }catch(InterruptedException ex) {? ? ? ? ? ? RLog.e("CrashHandler.InterruptedException--->"+ ex.toString());? ? ? ? }//退出程序Process.killProcess(Process.myPid());? ? ? ? System.exit(1);? ? }
Crash信息的獲取
獲取異常信息
/**
* 獲取捕獲異常的信息
*
* @param ex
*/privateStringcollectExceptionInfos(Throwable ex) {? ? ? ? Writer mWriter =newStringWriter();? ? ? ? PrintWriter mPrintWriter =newPrintWriter(mWriter);? ? ? ? ex.printStackTrace(mPrintWriter);? ? ? ? ex.printStackTrace();? ? ? ? Throwable mThrowable = ex.getCause();// 迭代棧隊(duì)列把所有的異常信息寫入writer中while(mThrowable !=null) {? ? ? ? ? ? mThrowable.printStackTrace(mPrintWriter);// 換行 每個(gè)個(gè)異常棧之間換行mPrintWriter.append("\r\n");? ? ? ? ? ? mThrowable = mThrowable.getCause();? ? ? ? }// 記得關(guān)閉mPrintWriter.close();returnmWriter.toString();? ? }
獲取應(yīng)用信息
/**
* 獲取應(yīng)用包參數(shù)信息
*/privatevoidcollectPackageInfos(Context context) {try{// 獲得包管理器PackageManager mPackageManager = context.getPackageManager();// 得到該應(yīng)用的信息,即主ActivityPackageInfo mPackageInfo = mPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);if(mPackageInfo !=null) {StringversionName = mPackageInfo.versionName ==null?"null": mPackageInfo.versionName;StringversionCode = mPackageInfo.versionCode +"";? ? ? ? ? ? ? ? mPackageInfos.put(VERSION_NAME, versionName);? ? ? ? ? ? ? ? mPackageInfos.put(VERSION_CODE, versionCode);? ? ? ? ? ? }? ? ? ? }catch(PackageManager.NameNotFoundException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }? ? }
獲取設(shè)備硬件信息(針對(duì)不同機(jī)型的用戶更有效地定位Bug)
/**
* 從系統(tǒng)屬性中提取設(shè)備硬件和版本信息
*/privatevoidcollectBuildInfos() {// 反射機(jī)制Field[] mFields = Build.class.getDeclaredFields();// 迭代Build的字段key-value 此處的信息主要是為了在服務(wù)器端手機(jī)各種版本手機(jī)報(bào)錯(cuò)的原因for(Field field : mFields) {try{? ? ? ? ? ? ? ? field.setAccessible(true);? ? ? ? ? ? ? ? mDeviceInfos.put(field.getName(), field.get("").toString());? ? ? ? ? ? }catch(IllegalArgumentException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }catch(IllegalAccessException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }? ? ? ? }? ? }
獲取系統(tǒng)常規(guī)信息(針對(duì)不同設(shè)定的用戶更有效定位Bug)
/**
* 獲取系統(tǒng)常規(guī)設(shè)定屬性
*/privatevoidcollectSystemInfos() {? ? ? ? Field[] fields = Settings.System.class.getFields();for(Field field : fields) {if(!field.isAnnotationPresent(Deprecated.class)? ? ? ? ? ? ? ? ? ? && field.getType() ==String.class) {try{Stringvalue = Settings.System.getString(mContext.getContentResolver(), (String) field.get(null));if(value !=null) {? ? ? ? ? ? ? ? ? ? ? ? mSystemInfos.put(field.getName(), value);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }catch(IllegalAccessException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }
獲取安全設(shè)置信息
/**
* 獲取系統(tǒng)安全設(shè)置信息
*/privatevoidcollectSecureInfos() {? ? ? ? Field[] fields = Settings.Secure.class.getFields();for(Field field : fields) {if(!field.isAnnotationPresent(Deprecated.class)? ? ? ? ? ? ? ? ? ? && field.getType() ==String.class? ? ? ? ? ? ? ? ? ? && field.getName().startsWith("WIFI_AP")) {try{Stringvalue = Settings.Secure.getString(mContext.getContentResolver(), (String) field.get(null));if(value !=null) {? ? ? ? ? ? ? ? ? ? ? ? mSecureInfos.put(field.getName(), value);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }catch(IllegalAccessException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }
獲取應(yīng)用內(nèi)存信息(需要權(quán)限)
/**
* 獲取內(nèi)存信息
*/privateStringcollectMemInfos() {? ? ? ? BufferedReader br =null;? ? ? ? StringBuffer sb =newStringBuffer();? ? ? ? ArrayList commandLine =newArrayList<>();? ? ? ? commandLine.add("dumpsys");? ? ? ? commandLine.add("meminfo");? ? ? ? commandLine.add(Integer.toString(Process.myPid()));try{? ? ? ? ? ? java.lang.Process process = Runtime.getRuntime()? ? ? ? ? ? ? ? ? ? .exec(commandLine.toArray(newString[commandLine.size()]));? ? ? ? ? ? br =newBufferedReader(newInputStreamReader(process.getInputStream()),8192);while(true) {Stringline = br.readLine();if(line ==null) {break;? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? sb.append(line);? ? ? ? ? ? ? ? sb.append("\n");? ? ? ? ? ? }? ? ? ? }catch(IOException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }finally{if(br !=null) {try{? ? ? ? ? ? ? ? ? ? br.close();? ? ? ? ? ? ? ? }catch(IOException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }returnsb.toString();? ? }
最后將這些信息儲(chǔ)存到infos中豪筝,以便之后我們回傳給上傳具體功能時(shí)候更加方便,有了這些數(shù)據(jù)敲街,我們應(yīng)該能夠快速定位崩潰的原因了
/**
* 獲取設(shè)備參數(shù)信息
*
* @param context
*/privatevoidcollectInfos(Context context, Throwable ex) {? ? ? ? mExceptionInfos = collectExceptionInfos(ex);? ? ? ? collectPackageInfos(context);? ? ? ? collectBuildInfos();? ? ? ? collectSystemInfos();? ? ? ? collectSecureInfos();? ? ? ? mMemInfos = collectMemInfos();//將信息儲(chǔ)存到一個(gè)總的Map中提供給上傳動(dòng)作回調(diào)infos.put(EXCEPETION_INFOS_STRING, mExceptionInfos);? ? ? ? infos.put(PACKAGE_INFOS_MAP, mPackageInfos);? ? ? ? infos.put(BUILD_INFOS_MAP, mDeviceInfos);? ? ? ? infos.put(SYSTEM_INFOS_MAP, mSystemInfos);? ? ? ? infos.put(SECURE_INFOS_MAP, mSecureInfos);? ? ? ? infos.put(MEMORY_INFOS_STRING, mMemInfos);? ? }
Crash日志寫入
將崩潰數(shù)據(jù)寫入到本地文件中(這里我只收集了異常信息和應(yīng)用信息多艇,具體情況可以根據(jù)自己需求來拼接其他數(shù)據(jù))
/**
* 將崩潰日志信息寫入本地文件
*/privateStringsaveCrashInfo2File() {? ? ? ? StringBuffer mStringBuffer = getInfosStr(mPackageInfos);? ? ? ? mStringBuffer.append(mExceptionInfos);// 保存文件峻黍,設(shè)置文件名StringmTime = formatter.format(newDate());StringmFileName ="CrashLog-"+ mTime +".log";if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {try{? ? ? ? ? ? ? ? File mDirectory =newFile(mDirPath);? ? ? ? ? ? ? ? Log.v(TAG, mDirectory.toString());if(!mDirectory.exists())? ? ? ? ? ? ? ? ? ? mDirectory.mkdirs();? ? ? ? ? ? ? ? FileOutputStream mFileOutputStream =newFileOutputStream(mDirectory + File.separator + mFileName);? ? ? ? ? ? ? ? mFileOutputStream.write(mStringBuffer.toString().getBytes());? ? ? ? ? ? ? ? mFileOutputStream.close();returnmFileName;? ? ? ? ? ? }catch(FileNotFoundException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }catch(IOException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }? ? ? ? }returnnull;? ? }/**
* 將HashMap遍歷轉(zhuǎn)換成StringBuffer
*/@NonNull? ? publicstaticStringBuffer getInfosStr(ConcurrentHashMap infos) {? ? ? ? StringBuffer mStringBuffer =newStringBuffer();for(Map.Entry entry : infos.entrySet()) {Stringkey = entry.getKey();Stringvalue = entry.getValue();? ? ? ? ? ? mStringBuffer.append(key +"="+ value +"\r\n");? ? ? ? }returnmStringBuffer;? ? }
由于每一個(gè)應(yīng)用上傳的服務(wù)器或者數(shù)據(jù)類型都不同,所以為了更好的延展性骨饿,我們使用接口回調(diào)台腥,將獲取的全部數(shù)據(jù)拋給應(yīng)用具體去實(shí)現(xiàn)(我這里為了演示黎侈,使用了Bmob后端云)
/**
* 上傳崩潰信息到服務(wù)器
*/publicvoiduploadCrashMessage(ConcurrentHashMap infos) {? ? ? ? mCrashUploader.uploadCrashMessage(infos);? ? }/**
* 崩潰信息上傳接口回調(diào)
*/public interface CrashUploader {voiduploadCrashMessage(ConcurrentHashMap infos);? ? }
使用方式
/**
* 初始化崩潰處理器
*/
privatevoidinitCrashHandler() {? ? ? ? mCrashUploader =newRCrashHandler.CrashUploader() {? ? ? ? ? ? @Override? ? ? ? ? ? publicvoiduploadCrashMessage(ConcurrentHashMap infos) {? ? ? ? ? ? ? ? CrashMessage cm =newCrashMessage();? ? ? ? ? ? ? ? ConcurrentHashMap packageInfos = (ConcurrentHashMap) infos.get(RCrashHandler.PACKAGE_INFOS_MAP);? ? ? ? ? ? ? ? cm.setDate(DateTimeUitl.getCurrentWithFormate(DateTimeUitl.sysDateFormate));? ? ? ? ? ? ? ? cm.setVersionName(packageInfos.get(RCrashHandler.VERSION_NAME));? ? ? ? ? ? ? ? cm.setVersionCode(packageInfos.get(RCrashHandler.VERSION_CODE));? ? ? ? ? ? ? ? cm.setExceptionInfos(((String) infos.get(RCrashHandler.EXCEPETION_INFOS_STRING)));? ? ? ? ? ? ? ? cm.setMemoryInfos((String) infos.get(RCrashHandler.MEMORY_INFOS_STRING));? ? ? ? ? ? ? ? cm.setDeviceInfos(RCrashHandler.getInfosStr((ConcurrentHashMap) infos? ? ? ? ? ? ? ? ? ? ? ? .get(RCrashHandler.BUILD_INFOS_MAP)).toString());? ? ? ? ? ? ? ? cm.setSystemInfoss(RCrashHandler.getInfosStr((ConcurrentHashMap) infos? ? ? ? ? ? ? ? ? ? ? ? .get(RCrashHandler.SYSTEM_INFOS_MAP)).toString());? ? ? ? ? ? ? ? cm.setSecureInfos(RCrashHandler.getInfosStr((ConcurrentHashMap) infos? ? ? ? ? ? ? ? ? ? ? ? .get(RCrashHandler.SECURE_INFOS_MAP)).toString());? ? ? ? ? ? ? ? cm.save(newSaveListener() {? ? ? ? ? ? ? ? ? ? @Override? ? ? ? ? ? ? ? ? ? publicvoiddone(Strings, BmobException e) {if(e ==null) {? ? ? ? ? ? ? ? ? ? ? ? ? ? RLog.e("上傳成功箕母!");? ? ? ? ? ? ? ? ? ? ? ? }else{? ? ? ? ? ? ? ? ? ? ? ? ? ? RLog.e("上傳Bmob失敗 錯(cuò)誤碼:"+ e.getErrorCode());? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? });? ? ? ? ? ? }? ? ? ? };? ? ? ? RCrashHandler.getInstance(FileUtils.getRootFilePath() +"EasySport/crashLog")? ? ? ? ? ? ? ? .init(mAppContext, mCrashUploader);? ? }
一些注意
使用過程中發(fā)現(xiàn)Process.killProcess(Process.myPid());和System.exit(1);會(huì)導(dǎo)致應(yīng)用自動(dòng)重啟嘶是,會(huì)影響一點(diǎn)用戶體驗(yàn)
所以我們使用了一個(gè)土方法聂喇,就是讓它去打開一個(gè)我們自己設(shè)定的CrashActivity來提高我們應(yīng)用的用戶體驗(yàn)
/**
* 自定義錯(cuò)誤處理,收集錯(cuò)誤信息 發(fā)送錯(cuò)誤報(bào)告等操作均在此完成.
*
* @param ex
* @return true:如果處理了該異常信息;否則返回false.
*/private boolean catchCrashException(Throwable ex) {if(ex ==null) {returnfalse;? ? ? ? }//啟動(dòng)我們自定義的頁面newThread() {? ? ? ? ? ? publicvoidrun() {//? ? ? ? ? ? ? ? Looper.prepare();//? ? ? ? ? ? ? ? Toast.makeText(mContext, "很抱歉,程序出現(xiàn)異常,即將退出", 0).show();//? ? ? ? ? ? ? ? Looper.loop();Intent intent =newIntent();? ? ? ? ? ? ? ? intent.setClass(mContext, CrashActivity.class);? ? ? ? ? ? ? ? intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);? ? ? ? ? ? ? ? ActivityCollector.finishAll();? ? ? ? ? ? ? ? mContext.startActivity(intent);? ? ? ? ? ? }? ? ? ? }.start();//收集設(shè)備參數(shù)信息collectInfos(mContext, ex);//保存日志文件saveCrashInfo2File();//上傳崩潰信息uploadCrashMessage(infos);returntrue;? ? }
CrashActivity的話就看個(gè)人需求了希太,可以使一段Sorry的文字或者一些交互的反饋操作都是可以的誊辉。
最后
CrashHandler整個(gè)寫下來思路是三步 :
1堕澄、異常捕獲
2霉咨、信息數(shù)據(jù)采集
3、 數(shù)據(jù)寫入本地和上傳服務(wù)器
轉(zhuǎn)載其他文章坑傅,但格式貌似有問題唁毒,如果需要看原文星爪,點(diǎn)擊https://juejin.im/post/59342ddd0ce46300571d95b5