CrashHandler

目录

认识与作用

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容