最近整理了一下错误日志捕捉的代码,这个虽然网上很多,但是加了点自己的想法吧!下面时代码:
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class CrashHandler implements UncaughtExceptionHandler {
//单例
private volatile static CrashHandler instance;
//异常日志路径
private File mLogPath;
//设备信息
private String mDeviceInfo;
//默认的主线程异常处理器
private UncaughtExceptionHandler mDefaultHandler;
//在application中初始化
public static void init(Context context) {
CrashHandler crashHandler = getInstance();
crashHandler.mLogPath = getCrashCacheDir(context);
crashHandler.mDeviceInfo = CrashHandler.getDeviceInfo(context);
crashHandler.mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//设置主线程处理器为自定义处理器
Thread.setDefaultUncaughtExceptionHandler(crashHandler);
}
//获取部分设备信息
private static String getDeviceInfo(Context context) {
StringBuilder sb = new StringBuilder();
try {
PackageManager pkgMgr = context.getPackageManager();
PackageInfo pkgInfo = pkgMgr.getPackageInfo(context.getPackageName(), 0);
sb.append("packageName: ").append(pkgInfo.packageName).append("\n");
sb.append("versionCode: ").append(pkgInfo.versionCode).append("\n");
sb.append("versionName: ").append(pkgInfo.versionName).append("\n");
} catch (Exception e) {
e.printStackTrace();
}
//厂商,例如Xiaomi,Meizu,Huawei等。
sb.append("brand: ").append(Build.BRAND).append("\n");
//产品型号全称,例如 meizu_mx3
sb.append("product: ").append(Build.PRODUCT).append("\n");
//产品型号名,例如 mx3
sb.append("device: ").append(Build.DEVICE).append("\n");
//安卓版本名,例如 4.4.4
sb.append("androidVersionName: ").append(Build.VERSION.RELEASE).append("\n");
//安卓API版本,例如 19
sb.append("androidApiVersion: ").append(Build.VERSION.SDK_INT).append("\n");
//通过反射记录Build所有数据
// Field[] fields = Build.class.getDeclaredFields();
// for (Field field : fields) {
// try {
// field.setAccessible(true);
// infos.put(field.getName(), field.get(null).toString());
// Log.d(TAG, field.getName() + " : " + field.get(null));
// } catch (Exception e) {
// Log.e(TAG, "an error occured when collect crash info", e);
// }
// }
return sb.toString();
}
//获取日志缓存目录,不用权限
private static File getCrashCacheDir(Context context) {
String cachePath;
if (context.getExternalCacheDir() != null) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getFilesDir().getPath();
}
File dir = new File(cachePath + File.separator + "log");
if (!dir.exists()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
return dir;
}
//DCL双重校验
public static CrashHandler getInstance() {
if (instance == null) {
synchronized (CrashHandler.class) {
if (instance == null) {
instance = new CrashHandler();
}
}
}
return instance;
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
//自行处理异常
handleException(ex);
//自行处理完再交给默认处理器
if (null != mDefaultHandler) {
mDefaultHandler.uncaughtException(thread, ex);
}
}
//处理异常
private void handleException(final Throwable throwable) {
StringBuilder sb = new StringBuilder();
//获取异常信息
getExceptionInfo(sb, throwable);
//附加设备信息
sb.append(mDeviceInfo);
//保存到文件
new Thread(()->saveCrash2File(sb.toString())).start();
}
//获取异常信息
private void getExceptionInfo(StringBuilder sb, Throwable throwable) {
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
//循环获取包装异常的异常原因
Throwable cause = throwable.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String exStr = writer.toString();
sb.append(exStr).append("\n");
}
//保存到文件
private void saveCrash2File(String crashInfo) {
try {
// 用于格式化日期,作为日志文件名的一部分
DateFormat formatter = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
String time = formatter.format(new Date());
String fileName = "crash_" + time + ".log";
String filePath = mLogPath.getAbsolutePath() + File.separator + fileName;
FileOutputStream fos = new FileOutputStream(filePath);
fos.write(crashInfo.getBytes());
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//获取所有日志文件
public static List<File> getCrashLogs() {
CrashHandler crashHandler = getInstance();
File crashDir = crashHandler.mLogPath;
//避免直接使用asList方法,产生的视图无法修改
return new ArrayList<>(Arrays.asList(crashDir.listFiles()));
}
//读取崩溃日志文件内容并返回
public static List<String> getCrashLogsStrings() {
List<String> logs = new ArrayList<>();
List<File> files = getCrashLogs();
FileInputStream inputStream;
for (File file : files) {
try {
inputStream = new FileInputStream(file);
int len = 0;
byte[] temp = new byte[1024];
StringBuilder sb = new StringBuilder("");
while ((len = inputStream.read(temp)) > 0){
sb.append(new String(temp, 0, len));
}
inputStream.close();
logs.add(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
return logs;
}
//删除日志文件
public static void removeCrashLogs(List<File> files) {
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
}
下面简单讲一下里面的一些东西。
错误日志捕捉
实际上程序崩溃就是主线程的崩溃,程序退出其实就是我们没有从主线程上捕获到异常,而 Java 的 Thread 类中含有一个 defaultUncaughtExceptionHandler 变量,该变量会处理线程发生的异常,并打印到默认输出上,也就是我们调试的时候出了问题可以在 log 中找到错误日志的原因。
// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
...
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
defaultUncaughtExceptionHandler = eh;
}
所以要捕获错误日志,我们只要实现 UncaughtExceptionHandler 接口,设置主线程的处理器为我们自定义的即可,UncaughtExceptionHandler 也定义在 Thread 类中,如下:
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
获取设备信息
大部分时候我们调试好的程序都运行没问题,可是一到客户那就又这又那的,这里涉及到一些设备的数据,我们的测试也不太可能覆盖所有机型,当出现问题的时候,我们希望把日志和设备信息一起提交上来。
下面时一些设备信息,PackageManager 可以获取软件版本号和版本名称,当然填入其他 flag 可以获得更多其他信息,我们这不细说。Build 类里面包含了一些设备信息,下面我选取了一部分使用,也可以和注释里面一样,通过反射获取全部数据。
//获取部分设备信息
private static String getDeviceInfo(Context context) {
StringBuilder sb = new StringBuilder();
try {
PackageManager pkgMgr = context.getPackageManager();
PackageInfo pkgInfo = pkgMgr.getPackageInfo(context.getPackageName(), 0);
sb.append("packageName: ").append(pkgInfo.packageName).append("\n");
sb.append("versionCode: ").append(pkgInfo.versionCode).append("\n");
sb.append("versionName: ").append(pkgInfo.versionName).append("\n");
} catch (Exception e) {
e.printStackTrace();
}
//厂商,例如Xiaomi,Meizu,Huawei等。
sb.append("brand: ").append(Build.BRAND).append("\n");
//产品型号全称,例如 meizu_mx3
sb.append("product: ").append(Build.PRODUCT).append("\n");
//产品型号名,例如 mx3
sb.append("device: ").append(Build.DEVICE).append("\n");
//安卓版本名,例如 4.4.4
sb.append("androidVersionName: ").append(Build.VERSION.RELEASE).append("\n");
//安卓API版本,例如 19
sb.append("androidApiVersion: ").append(Build.VERSION.SDK_INT).append("\n");
//通过反射记录Build所有数据
// Field[] fields = Build.class.getDeclaredFields();
// for (Field field : fields) {
// try {
// field.setAccessible(true);
// infos.put(field.getName(), field.get(null).toString());
// Log.d(TAG, field.getName() + " : " + field.get(null));
// } catch (Exception e) {
// Log.e(TAG, "an error occured when collect crash info", e);
// }
// }
return sb.toString();
}
获取异常信息
//获取异常信息
private void getExceptionInfo(StringBuilder sb, Throwable throwable) {
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
//循环获取包装异常的异常原因
Throwable cause = throwable.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String exStr = writer.toString();
sb.append(exStr).append("\n");
}
这里获取异常我们需要使用 Throwable 这个类,因为我们说的异常可能包括了 Error 和 Exception,都需要捕获。
由于异常可能包含包装的 case,所有我们需要循环一次拿到最原始的异常信息。
有关异常的详细信息,可以看我另一篇博客:
https://blog.csdn.net/lfq88/article/details/107173268
保存日志
//获取日志缓存目录,不用权限
private static File getCrashCacheDir(Context context) {
String cachePath;
if (context.getExternalCacheDir() != null) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getFilesDir().getPath();
}
File dir = new File(cachePath + File.separator + "log");
if (!dir.exists()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
return dir;
}
日志上传
关于日志的上传我并不想写在这里面,而是更希望在外部拿到日志数据后再处理,下面向外提供了获取异常数据的方法,另外提供了一个删除方法,目的是上传日志成功后,用来清除保存下拉的文件。
//获取所有日志文件
public static List<File> getCrashLogs() {
CrashHandler crashHandler = getInstance();
File crashDir = crashHandler.mLogPath;
//避免直接使用asList方法,产生的视图无法修改
return new ArrayList<>(Arrays.asList(crashDir.listFiles()));
}
//读取崩溃日志文件内容并返回
public static List<String> getCrashLogsStrings() {
List<String> logs = new ArrayList<>();
List<File> files = getCrashLogs();
FileInputStream inputStream;
for (File file : files) {
try {
inputStream = new FileInputStream(file);
int len = 0;
byte[] temp = new byte[1024];
StringBuilder sb = new StringBuilder("");
while ((len = inputStream.read(temp)) > 0){
sb.append(new String(temp, 0, len));
}
inputStream.close();
logs.add(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
return logs;
}
//删除日志文件
public static void removeCrashLogs(List<File> files) {
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
结语
东西写的不多,希望对读者有帮助,自己也记录一下。
end