撸一个项目必备的CrashHandler
上周工作中新来的小伙伴问了一下项目中CrashHandler,当时只是简单讲了一下
周末到了,心血来潮,手把手撸一个好用全面的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(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);
}
Crash信息的获取
- 获取异常信息
/**
* 获取捕获异常的信息
*
* @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();
}
}
- 获取设备硬件信息(针对不同机型的用户更有效地定位Bug)
/**
* 从系统属性中提取设备硬件和版本信息
*/
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();
}
}
}
- 获取系统常规信息(针对不同设定的用户更有效定位Bug)
/**
* 获取系统常规设定属性
*/
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);
}
Crash日志写入
- 将崩溃数据写入到本地文件中(这里我只收集了异常信息 和 应用信息,具体情况可以根据自己需求来拼接其他数据)
/**
* 将崩溃日志信息写入本地文件
*/
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;
}
- 由于每一个应用上传的服务器或者数据类型都不同,所以为了更好的延展性,我们使用接口回调,将获取的全部数据抛给应用具体去实现(我这里为了演示,使用了Bmob后端云)
/**
* 上传崩溃信息到服务器
*/
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);
会导致应用自动重启三次,会影响一点用户体验
所以我们使用了一个土方法,就是让它去打开一个我们自己设定的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整个写下来思路是三步 :
1、异常捕获
2、信息数据采集
3、 数据写入本地和上传服务器
项目地址:Github地址
这是我一个随便写写的项目
CrashHandler主要在rbase的util,还有app的MyApplication 中应用到