上周工做中新来的小伙伴问了一下项目中CrashHandler,当时只是简单讲了一下
周末到了,心血来潮,手把手撸一个好用全面的CrashHandler吧,对于之后项目开发和当前项目的完善也有必定的帮助。javascript
CrashHandler
: 崩溃处理器,捕获Crash信息并做出相应的处理java
CrashHandler
来将Crash写入到本地方便咱们随时随地查看。CrashHandler
来帮咱们将崩溃信息返回给后台,以便及时修复。下面咱们就手把手写一个实用、本地化、轻量级的CrashHandler吧。git
Thread.UncaughtExceptionHandler
接口,并重写uncaughtException
方法,此时你的CrashHandler就具有了接收处理异常的能力了。Thread.setDefaultUncaughtExceptionHandler(CrashHandler)
,来使用咱们自定义的CrashHandler
来取代系统默认的CrashHandler
private RCrashHandler(String dirPath) {
mDirPath = dirPath;
File mDirectory = new File(mDirPath);
if (!mDirectory.exists()) {
mDirectory.mkdirs();
}
}
public static RCrashHandler getInstance(String dirPath) {
if (INSTANCE == null) {
synchronized (RCrashHandler.class) {
if (INSTANCE == null) {
INSTANCE = new RCrashHandler(dirPath);
}
}
}
return INSTANCE;
}
/** * 初始化 * * @param context 上下文 * @param crashUploader 崩溃信息上传接口回调 */
public void init(Context context, CrashUploader crashUploader) {
mCrashUploader = crashUploader;
mContext = context;
//保存一份系统默认的CrashHandler
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//使用咱们自定义的异常处理器替换程序默认的
Thread.setDefaultUncaughtExceptionHandler(this);
}
/** * 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用uncaughtException方法 * * @param t 出现未捕获异常的线程 * @param e 未捕获的异常,有了这个ex,咱们就能够获得异常信息 */
@Override
public void uncaughtException(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) {
return false;
}
new Thread() {
public void run() {
// Looper.prepare();
// Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", 0).show();
// Looper.loop();
Intent intent = new Intent();
intent.setClass(mContext, CrashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityCollector.finishAll();
mContext.startActivity(intent);
}
}.start();
//收集设备参数信息
collectInfos(mContext, ex);
//保存日志文件
saveCrashInfo2File();
//上传崩溃信息
uploadCrashMessage(infos);
return true;
}
/** * 退出应用 */
public static void killProcess() {
//结束应用
new Thread(new Runnable() {
@Override
public void run() {
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);
}复制代码
/** * 获取捕获异常的信息 * * @param ex */
private String collectExceptionInfos(Throwable ex) {
Writer mWriter = new StringWriter();
PrintWriter mPrintWriter = new PrintWriter(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();
return mWriter.toString();
}复制代码
/** * 获取应用包参数信息 */
private void collectPackageInfos(Context context) {
try {
// 得到包管理器
PackageManager mPackageManager = context.getPackageManager();
// 获得该应用的信息,即主Activity
PackageInfo mPackageInfo = mPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (mPackageInfo != null) {
String versionName = mPackageInfo.versionName == null ? "null" : mPackageInfo.versionName;
String versionCode = mPackageInfo.versionCode + "";
mPackageInfos.put(VERSION_NAME, versionName);
mPackageInfos.put(VERSION_CODE, versionCode);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}复制代码
/** * 从系统属性中提取设备硬件和版本信息 */
private void collectBuildInfos() {
// 反射机制
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();
}
}
}复制代码
/** * 获取系统常规设定属性 */
private void collectSystemInfos() {
Field[] fields = Settings.System.class.getFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Deprecated.class)
&& field.getType() == String.class) {
try {
String value = Settings.System.getString(mContext.getContentResolver(), (String) field.get(null));
if (value != null) {
mSystemInfos.put(field.getName(), value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}复制代码
/** * 获取系统安全设置信息 */
private void collectSecureInfos() {
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 {
String value = Settings.Secure.getString(mContext.getContentResolver(), (String) field.get(null));
if (value != null) {
mSecureInfos.put(field.getName(), value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}复制代码
/** * 获取内存信息 */
private String collectMemInfos() {
BufferedReader br = null;
StringBuffer sb = new StringBuffer();
ArrayList<String> commandLine = new ArrayList<>();
commandLine.add("dumpsys");
commandLine.add("meminfo");
commandLine.add(Integer.toString(Process.myPid()));
try {
java.lang.Process process = Runtime.getRuntime()
.exec(commandLine.toArray(new String[commandLine.size()]));
br = new BufferedReader(new InputStreamReader(process.getInputStream()), 8192);
while (true) {
String line = 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();
}
}
}
return sb.toString();
}复制代码
infos
中,以便以后咱们回传给上传具体功能时候更加方便,有了这些数据,咱们应该可以快速定位崩溃的缘由了/** * 获取设备参数信息 * * @param context */
private void collectInfos(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);
}复制代码
/** * 将崩溃日志信息写入本地文件 */
private String saveCrashInfo2File() {
StringBuffer mStringBuffer = getInfosStr(mPackageInfos);
mStringBuffer.append(mExceptionInfos);
// 保存文件,设置文件名
String mTime = formatter.format(new Date());
String mFileName = "CrashLog-" + mTime + ".log";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
try {
File mDirectory = new File(mDirPath);
Log.v(TAG, mDirectory.toString());
if (!mDirectory.exists())
mDirectory.mkdirs();
FileOutputStream mFileOutputStream = new FileOutputStream(mDirectory + File.separator + mFileName);
mFileOutputStream.write(mStringBuffer.toString().getBytes());
mFileOutputStream.close();
return mFileName;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/** * 将HashMap遍历转换成StringBuffer */
@NonNull
public static StringBuffer getInfosStr(ConcurrentHashMap<String, String> infos) {
StringBuffer mStringBuffer = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
mStringBuffer.append(key + "=" + value + "\r\n");
}
return mStringBuffer;
}复制代码
/** * 上传崩溃信息到服务器 */
public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
mCrashUploader.uploadCrashMessage(infos);
}
/** * 崩溃信息上传接口回调 */
public interface CrashUploader {
void uploadCrashMessage(ConcurrentHashMap<String, Object> infos);
}复制代码
/** * 初始化崩溃处理器 */
private void initCrashHandler() {
mCrashUploader = new RCrashHandler.CrashUploader() {
@Override
public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
CrashMessage cm = new CrashMessage();
ConcurrentHashMap<String, String> packageInfos = (ConcurrentHashMap<String, String>) 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<String, String>) infos
.get(RCrashHandler.BUILD_INFOS_MAP)).toString());
cm.setSystemInfoss(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
.get(RCrashHandler.SYSTEM_INFOS_MAP)).toString());
cm.setSecureInfos(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
.get(RCrashHandler.SECURE_INFOS_MAP)).toString());
cm.save(new SaveListener<String>() {
@Override
public void done(String s, BmobException e) {
if (e == null) {
RLog.e("上传成功!");
} else {
RLog.e("上传Bmob失败 错误码:" + e.getErrorCode());
}
}
});
}
};
RCrashHandler.getInstance(FileUtils.getRootFilePath() + "EasySport/crashLog")
.init(mAppContext, mCrashUploader);
}复制代码
使用过程当中发如今Activity中 Process.killProcess(Process.myPid());
和System.exit(1);
会致使应用自动重启三次,会影响一点用户体验github
因此咱们使用了一个土方法,就是让它去打开一个咱们本身设定的CrashActivity
来提升咱们应用的用户体验后端
/** * 自定义错误处理,收集错误信息 发送错误报告等操做均在此完成. * * @param ex * @return true:若是处理了该异常信息;不然返回false. */
private boolean catchCrashException(Throwable ex) {
if (ex == null) {
return false;
}
//启动咱们自定义的页面
new Thread() {
public void run() {
// Looper.prepare();
// Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", 0).show();
// Looper.loop();
Intent intent = new Intent();
intent.setClass(mContext, CrashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityCollector.finishAll();
mContext.startActivity(intent);
}
}.start();
//收集设备参数信息
collectInfos(mContext, ex);
//保存日志文件
saveCrashInfo2File();
//上传崩溃信息
uploadCrashMessage(infos);
return true;
}复制代码
CrashActivity
的话就看我的需求了,可使一段Sorry的文字或者一些交互的反馈操做都是能够的。安全
CrashHandler整个写下来思路是三步 :
一、异常捕获
二、信息数据采集
三、 数据写入本地和上传服务器
项目地址:Github地址
这是我一个随便写写的项目
CrashHandler主要在rbase的util,还有app的MyApplication 中应用到服务器