前言
在日常开发中,如果遇到Android程序崩溃,我们只需要打开AndroidStudio的控制台的Logcat便能查看到程序的崩溃信息。
可是当程序上线后,如果出现程序崩溃的情况,我们可能很难找到问题。这就需要我们的程序能够自己收集到崩溃的异常信息,然后再适当的时候将这些信息上传到服务器,然后我们获取到这些异常信息后,在下个更新的版本将其修复。
思路
- Android使用Thread.UncaughtExceptionHandler接口类来处理程序崩溃的情况,我们也可以通过该接口实现程序崩溃时的信息收集等操作。
- 单例模式实现,初始化获取系统默认的UncaughtException处理器,可以将部分操作交给默认处理器处理,然后设置CrashHandler为程序的默认处理器。
- 实现接口方法uncaughtException(Thread thread, Throwable ex),收集程序,设备,崩溃异常等信息。收集完信息后,交给系统自己处理。
- 为方便查看,收集的信息存储到txt文件中,默认保存在sd卡根目录/你的app_name/Crash/ (如:
/storage/emulated/0/ErrorCatch/Crash/2018-09-21 09:42:59.text
) - getCrashReportFiles(Context ctx)方法可以返回所有的错误信息文件路径,可以根据文件路径上传到服务器,然后将其删除,防止重复上传。
代码&Demo
GitHub:https://github.com/DeMonLiu623/CrashHandler
效果
在Demo中我们模拟实现了一个数组越界异常,收集到的异常信息如下图:
实现
代码很简单,而且注释很详细。
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
/**
* 系统默认的UncaughtException处理类
*/
private Thread.UncaughtExceptionHandler mDefaultHandler;
/**
* 程序的Context对象
*/
private Context mContext;
/**
* 错误报告文件的扩展名
*/
private static final String CRASH_REPORTER_EXTENSION = ".text";
/**
* CrashHandler实例
*/
private static CrashHandler INSTANCE;
/**
* 保证只有一个CrashHandler实例
*/
private CrashHandler() {
}
/**
* 获取CrashHandler实例 ,单例模式
*/
public static CrashHandler getInstance() {
if (INSTANCE == null) {
synchronized (CrashHandler.class) {
if (INSTANCE == null) {
INSTANCE = new CrashHandler();
}
}
}
return INSTANCE;
}
/**
* 初始化,注册Context对象,
* 获取系统默认的UncaughtException处理器,可以将部分操作交给默认处理器处理
* 设置该CrashHandler为程序的默认处理器
*
* @param ctx
*/
public void init(Context ctx) {
mContext = ctx;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当UncaughtException发生时会转入该函数来处理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
handleException(ex);
if (mDefaultHandler != null) {
//收集完信息后,交给系统自己处理崩溃
mDefaultHandler.uncaughtException(thread, ex);
}
}
/**
* 自定义错误处理,收集错误信息
* 发送错误报告等操作均在此完成.
* 开发者可以根据自己的情况来自定义异常处理逻辑
*/
private void handleException(Throwable ex) {
if (ex == null) {
Log.w(TAG, "handleException--- ex==null");
return;
}
String msg = ex.getLocalizedMessage();
if (msg == null) {
return;
}
//收集设备信息
//保存错误报告文件
saveCrashInfoToFile(ex);
}
/**
* 获取错误报告文件路径
*
* @param ctx
* @return
*/
public static String[] getCrashReportFiles(Context ctx) {
File filesDir = new File(getCrashFilePath(ctx));
String[] fileNames = filesDir.list();
int length = fileNames.length;
String[] filePaths = new String[length];
for (int i = 0; i < length; i++) {
filePaths[i] = getCrashFilePath(ctx) + fileNames[i];
}
return filePaths;
}
/**
* 保存错误信息到文件中
*
* @param ex
* @return
*/
private void saveCrashInfoToFile(Throwable ex) {
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
String result = info.toString();
printWriter.close();
StringBuilder sb = new StringBuilder();
@SuppressLint("SimpleDateFormat") SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
String now = sdf.format(new Date());
sb.append("TIME:").append(now);//崩溃时间
//程序信息
sb.append("\nAPPLICATION_ID:").append(BuildConfig.APPLICATION_ID);//软件APPLICATION_ID
sb.append("\nVERSION_CODE:").append(BuildConfig.VERSION_CODE);//软件版本号
sb.append("\nVERSION_NAME:").append(BuildConfig.VERSION_NAME);//VERSION_NAME
sb.append("\nBUILD_TYPE:").append(BuildConfig.BUILD_TYPE);//是否是DEBUG版本
//设备信息
sb.append("\nMODEL:").append(android.os.Build.MODEL);
sb.append("\nRELEASE:").append(Build.VERSION.RELEASE);
sb.append("\nSDK:").append(Build.VERSION.SDK_INT);
sb.append("\nEXCEPTION:").append(ex.getLocalizedMessage());
sb.append("\nSTACK_TRACE:").append(result);
try {
FileWriter writer = new FileWriter(getCrashFilePath(mContext) + now + CRASH_REPORTER_EXTENSION);
writer.write(sb.toString());
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取文件夹路径
*
* @param context
* @return
*/
private static String getCrashFilePath(Context context) {
String path = null;
try {
path = Environment.getExternalStorageDirectory().getCanonicalPath() + "/" + context.getResources().getString(R.string.app_name) + "/Crash/";
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
}
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
/**
* 系统默认的UncaughtException处理类
*/
private Thread.UncaughtExceptionHandler mDefaultHandler;
/**
* 程序的Context对象
*/
private Context mContext;
/**
* 错误报告文件的扩展名
*/
private static final String CRASH_REPORTER_EXTENSION = ".text";
/**
* CrashHandler实例
*/
private static CrashHandler INSTANCE;
/**
* 保证只有一个CrashHandler实例
*/
private CrashHandler() {
}
/**
* 获取CrashHandler实例 ,单例模式
*/
public static CrashHandler getInstance() {
if (INSTANCE == null) {
synchronized (CrashHandler.class) {
if (INSTANCE == null) {
INSTANCE = new CrashHandler();
}
}
}
return INSTANCE;
}
/**
* 初始化,注册Context对象,
* 获取系统默认的UncaughtException处理器,可以将部分操作交给默认处理器处理
* 设置该CrashHandler为程序的默认处理器
*
* @param ctx
*/
public void init(Context ctx) {
mContext = ctx;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当UncaughtException发生时会转入该函数来处理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
handleException(ex);
if (mDefaultHandler != null) {
//收集完信息后,交给系统自己处理崩溃
mDefaultHandler.uncaughtException(thread, ex);
}
}
/**
* 自定义错误处理,收集错误信息
* 发送错误报告等操作均在此完成.
* 开发者可以根据自己的情况来自定义异常处理逻辑
*/
private void handleException(Throwable ex) {
if (ex == null) {
Log.w(TAG, "handleException--- ex==null");
return;
}
String msg = ex.getLocalizedMessage();
if (msg == null) {
return;
}
//收集设备信息
//保存错误报告文件
saveCrashInfoToFile(ex);
}
/**
* 获取错误报告文件路径
*
* @param ctx
* @return
*/
public static String[] getCrashReportFiles(Context ctx) {
File filesDir = new File(getCrashFilePath(ctx));
String[] fileNames = filesDir.list();
int length = fileNames.length;
String[] filePaths = new String[length];
for (int i = 0; i < length; i++) {
filePaths[i] = getCrashFilePath(ctx) + fileNames[i];
}
return filePaths;
}
/**
* 保存错误信息到文件中
*
* @param ex
* @return
*/
private void saveCrashInfoToFile(Throwable ex) {
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
String result = info.toString();
printWriter.close();
StringBuilder sb = new StringBuilder();
@SuppressLint("SimpleDateFormat") SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
String now = sdf.format(new Date());
sb.append("TIME:").append(now);//崩溃时间
//程序信息
sb.append("\nAPPLICATION_ID:").append(BuildConfig.APPLICATION_ID);//软件APPLICATION_ID
sb.append("\nVERSION_CODE:").append(BuildConfig.VERSION_CODE);//软件版本号
sb.append("\nVERSION_NAME:").append(BuildConfig.VERSION_NAME);//VERSION_NAME
sb.append("\nBUILD_TYPE:").append(BuildConfig.BUILD_TYPE);//是否是DEBUG版本
//设备信息
sb.append("\nMODEL:").append(android.os.Build.MODEL);
sb.append("\nRELEASE:").append(Build.VERSION.RELEASE);
sb.append("\nSDK:").append(Build.VERSION.SDK_INT);
sb.append("\nEXCEPTION:").append(ex.getLocalizedMessage());
sb.append("\nSTACK_TRACE:").append(result);
try {
FileWriter writer = new FileWriter(getCrashFilePath(mContext) + now + CRASH_REPORTER_EXTENSION);
writer.write(sb.toString());
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取文件夹路径
*
* @param context
* @return
*/
private static String getCrashFilePath(Context context) {
String path = null;
try {
path = Environment.getExternalStorageDirectory().getCanonicalPath() + "/" + context.getResources().getString(R.string.app_name) + "/Crash/";
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
}