目录
认识与作用
Crash的捕获
Crash信息的获取
Crash日志写入上传
使用方式
一些注意
最后
认识与作用
CrashHandler: 崩溃处理器,捕获Crash信息并作出相应的处理
测试使用:应用在日常的开发中,我们经常需要去Logcat测试我们的App,但由于很多原因,Android Monitor会闪屏或者Crash信息丢失。 这个时候就需要一个CrashHandler来将Crash写入到本地方便我们随时随地查看。
上线使用:应用的崩溃率是用户衡量筛选应用的重要标准,那么应用上线以后 我们无法向用户借手机来分析崩溃原因。为了减低崩溃率,这个时候需要CrashHandler来帮我们将崩溃信息返回给后台,以便及时修复。
下面我们就手把手写一个实用、本地化、轻量级的CrashHandler吧。
Crash的捕获
实现Thread.UncaughtExceptionHandler接口,并重写uncaughtException方法,此时你的CrashHandler就具备了接收处理异常的能力了。
调用Thread.setDefaultUncaughtExceptionHandler(CrashHandler),来使用我们自定义的CrashHandler来取代系统默认的CrashHandler
结合单例模式
总体三步: 捕获异常、信息数据获取、数据写入和上传
总体的初始化代码如下:
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 崩溃信息上传接口回调
*/publicvoidinit(Context context, CrashUploader crashUploader) { mCrashUploader = crashUploader; mContext = context;//保存一份系统默认的CrashHandlermDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();//使用我们自定义的异常处理器替换程序默认的Thread.setDefaultUncaughtExceptionHandler(this); }/**
* 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用uncaughtException方法
*
* @param t 出现未捕获异常的线程
* @param e 未捕获的异常,有了这个ex,我们就可以得到异常信息
*/@Override publicvoiduncaughtException(Thread t, Throwable e) {if(!catchCrashException(e) && mDefaultHandler !=null) {//没有自定义的CrashHandler的时候就调用系统默认的异常处理方式mDefaultHandler.uncaughtException(t, e); }else{//退出应用killProcess(); } }/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* @return true:如果处理了该异常信息;否则返回false.
*/private boolean catchCrashException(Throwable ex) {if(ex ==null) {returnfalse; }newThread() { publicvoidrun() {// Looper.prepare();// Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", 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();//收集设备参数信息collectInfos(mContext, ex);//保存日志文件saveCrashInfo2File();//上传崩溃信息uploadCrashMessage(infos);returntrue; }/**
* 退出应用
*/publicstaticvoidkillProcess() {//结束应用newThread(newRunnable() { @Override publicvoidrun() { Looper.prepare(); ToastUtils.showLong("哎呀,程序发生异常啦..."); 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();// 迭代栈队列把所有的异常信息写入writer中while(mThrowable !=null) { mThrowable.printStackTrace(mPrintWriter);// 换行 每个个异常栈之间换行mPrintWriter.append("\r\n"); mThrowable = mThrowable.getCause(); }// 记得关闭mPrintWriter.close();returnmWriter.toString(); }
获取应用信息
/**
* 获取应用包参数信息
*/privatevoidcollectPackageInfos(Context context) {try{// 获得包管理器PackageManager mPackageManager = context.getPackageManager();// 得到该应用的信息,即主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(); } }
获取设备硬件信息(针对不同机型的用户更有效地定位Bug)
/**
* 从系统属性中提取设备硬件和版本信息
*/privatevoidcollectBuildInfos() {// 反射机制Field[] mFields = Build.class.getDeclaredFields();// 迭代Build的字段key-value 此处的信息主要是为了在服务器端手机各种版本手机报错的原因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(); } } }
获取系统常规信息(针对不同设定的用户更有效定位Bug)
/**
* 获取系统常规设定属性
*/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(); } } } }
获取安全设置信息
/**
* 获取系统安全设置信息
*/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(); } } } }
获取应用内存信息(需要权限)
/**
* 获取内存信息
*/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(); }
最后将这些信息储存到infos中,以便之后我们回传给上传具体功能时候更加方便,有了这些数据,我们应该能够快速定位崩溃的原因了
/**
* 获取设备参数信息
*
* @param context
*/privatevoidcollectInfos(Context context, Throwable ex) { mExceptionInfos = collectExceptionInfos(ex); collectPackageInfos(context); collectBuildInfos(); collectSystemInfos(); collectSecureInfos(); mMemInfos = collectMemInfos();//将信息储存到一个总的Map中提供给上传动作回调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日志写入
将崩溃数据写入到本地文件中(这里我只收集了异常信息和应用信息,具体情况可以根据自己需求来拼接其他数据)
/**
* 将崩溃日志信息写入本地文件
*/privateStringsaveCrashInfo2File() { StringBuffer mStringBuffer = getInfosStr(mPackageInfos); mStringBuffer.append(mExceptionInfos);// 保存文件,设置文件名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遍历转换成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; }
由于每一个应用上传的服务器或者数据类型都不同,所以为了更好的延展性,我们使用接口回调,将获取的全部数据抛给应用具体去实现(我这里为了演示,使用了Bmob后端云)
/**
* 上传崩溃信息到服务器
*/publicvoiduploadCrashMessage(ConcurrentHashMap infos) { mCrashUploader.uploadCrashMessage(infos); }/**
* 崩溃信息上传接口回调
*/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失败 错误码:"+ e.getErrorCode()); } } }); } }; RCrashHandler.getInstance(FileUtils.getRootFilePath() +"EasySport/crashLog") .init(mAppContext, mCrashUploader); }
一些注意
使用过程中发现Process.killProcess(Process.myPid());和System.exit(1);会导致应用自动重启,会影响一点用户体验
所以我们使用了一个土方法,就是让它去打开一个我们自己设定的CrashActivity来提高我们应用的用户体验
/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* @return true:如果处理了该异常信息;否则返回false.
*/private boolean catchCrashException(Throwable ex) {if(ex ==null) {returnfalse; }//启动我们自定义的页面newThread() { publicvoidrun() {// Looper.prepare();// Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", 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();//收集设备参数信息collectInfos(mContext, ex);//保存日志文件saveCrashInfo2File();//上传崩溃信息uploadCrashMessage(infos);returntrue; }
CrashActivity的话就看个人需求了,可以使一段Sorry的文字或者一些交互的反馈操作都是可以的。
最后
CrashHandler整个写下来思路是三步 :
1、异常捕获
2、信息数据采集
3、 数据写入本地和上传服务器
转载其他文章,但格式貌似有问题,如果需要看原文,点击https://juejin.im/post/59342ddd0ce46300571d95b5