【版权申明】非商业目的可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/101061653
出自:shusheng007
文章目录
概述
在Android开发中任何App都存在crash的可能性,所以当奔溃后如何获得有用信息进而修复这个问题,防止再次发生成了我们必须面对的问题。Android发展到现在,开发工具与最初时期已经不可同日而语了,就是关于这个关于崩溃的报告工具也是多如牛毛,我自己用过的就包括腾讯的 buggly,Google的fabric,百度的一个奔溃工具,那我们今天就稍微理解一下他们的工作原理。
Android (其实是Android 的JVM的功能)本身提供了一套处理由于未捕获的异常引起的奔溃机制,那就是使用下面这个定义在Thread
类内部的接口。
@FunctionalInterface
public interface UncaughtExceptionHandler{
void uncaughtException(Thread t, Throwable e);
}
这个接口的作用是当一个线程由于未捕获的异常而突然中止时,会回调其方法uncaughtException()
. 市面上比较流行的崩溃监控工具都是基于这个原理开发的,当捕获了奔溃后将异常信息上传至其服务器,例如腾讯的 buggly,Google的fabric。
今天我们就详细了解一下这个接口,并写一个自己的异常处理器,这个异常处理器也是存在实际意义的,可以协助日常的debug工作。
原理
假设我们有一个线程Thread1
, 其属于ThreadGroup1
(java中每个线程都必须隶属于一个ThreadGroup
),其由于未捕获的NullPointerException
而崩溃了,虚拟机执行的步骤如下:
- 先查看
Thread1
有没有设置UncaughtExceptionHandler
,有的话就调用其uncaughtException()
方法处理异常 - 否则就调用
ThreadGroup1
的uncaughtException()
方法处理异常,这个方法的执行逻辑如下
a 先查看ThreadGroup1
有没有父ThreadGroup
,有则调用其父ThreadGroup
的uncaughtException()
,
b 否则查看是否存在线程的默认处理器DefaultUncaughtExceptionHandler
,这个处理器是对当前进程的所有线程起作用的。存在则调用其方法uncaughtException()
方法处理异常。
c否则检查异常是否是ThreadDeath
类型,如果是则不做任何处理,如果不是则打印异常到输出窗口
原理清楚了,我开始设计一个自己的异常处理器。我们要求当发生奔溃时,可以生成奔溃报告并
自定义uncaughtExceptionHandler
/**
* Created by shusheng007
*/
public class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "UncaughtExceptionHandler ";
private Thread.UncaughtExceptionHandler mDefaultHandler;
private Context mContext;
private String reportLocation;
private ExecutorService mService = Executors.newSingleThreadExecutor();
private UncaughtExceptionHandler () {
}
public static UncaughtExceptionHandler getInstance() {
return InstanceMaker.instance;
}
public String getReportDefaultLocation(@NonNull Context context) {
return context.getExternalFilesDir(null).getPath() + "/crashReports/";
}
public void init(Context context) {
init(context, "");
}
public void init(Context context, String reportLocation) {
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
if (TextUtils.isEmpty(reportLocation)) {
this.reportLocation = getReportDefaultLocation(context);
} else if (reportLocation.endsWith("/")) {
this.reportLocation = reportLocation + "crashReports/";
} else {
this.reportLocation = reportLocation + "/crashReports/";
}
}
public void setReportLocation(String reportLocation) {
this.reportLocation = reportLocation;
}
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
Future<Boolean> future = mService.submit(() -> {
save2File(reportLocation, generateReport(throwable).toString() + "\n\n");
return true;
});
try {
if (future.get().booleanValue()) {
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, throwable);
} else {
android.os.Process.killProcess(android.os.Process.myPid());
}
}
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, e.getMessage(), e);
}
}
private void save2File(String reportLocation, String crashReport) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return;
}
File dir = new File(reportLocation);
if (!dir.exists()) {
dir.mkdir();
}
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, -30);
for (File file : dir.listFiles()) {
if (!file.isFile()) {
continue;
}
try {
if (dateFormat.parse(file.getName().replace(".txt", "")).before(calendar.getTime())) {
file.delete();
}
} catch (ParseException e) {
Log.e(TAG, e.getMessage(), e);
}
}
String fileName = dateFormat.format(Calendar.getInstance().getTime()) + ".txt";
File file = new File(dir, fileName);
try (FileOutputStream fos = new FileOutputStream(file, true)) {
fos.write(crashReport.getBytes());
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
private PackageInfo getPackageInfo(Context context) {
PackageInfo info = null;
try {
info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
info = new PackageInfo();
}
return info;
}
private Report generateReport(Throwable e) {
PackageInfo packageInfo = getPackageInfo(mContext);
StackTraceElement[] elements = e.getStackTrace();
StringBuilder sb = new StringBuilder();
for (StackTraceElement element : elements) {
sb.append(element.toString() + "\n");
}
final Runtime runtime = Runtime.getRuntime();
final long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
final long maxHeapSize = runtime.maxMemory() / (1024 * 1024);
final long availableHeapSize = maxHeapSize - usedMemory;
return new Report.Builder(Calendar.getInstance().getTime())
.setVersionName(packageInfo.versionName)
.setOsVersion(Build.VERSION.RELEASE)
.setDeviceBrand(Build.MANUFACTURER)
.setUsedMemory(usedMemory)
.setAvailableHeepSize(availableHeapSize)
.setErrorMessage(e.getMessage())
.setInvokeStackInfo(sb.toString())
.build();
}
private static class InstanceMaker {
private static UncaughtExceptionHandler instance = new UncaughtExceptionHandler ();
}
}
上面的代码其实已经比较清楚了,我们在此稍作解释
1:通过init()
方法将当前注册handler注册到所有线程的上,并将线程默认的处理器保存到mDefaultHandler
上
2:在uncaughtException()
中构建错误报告并保存到本地,然后调用mDefaultHandler
的uncaughtException()
方法
当奔溃发生时,我们就会收集奔溃设备的各种信息,写入按天组织的文件中,即同一天的崩溃均会在一个文件中,设置日志最长保留时间。
需要注意的是在异常处理过程中是存在一些技巧的,不然有可能造成其他的异常分析库工作异常。我要先将线程默认的handler保存下来,待 我们这捕获了异常并且处理完成(写入文件)后,调用保存下来的处理器,这就给了其他处理器执行的机会。
例如我们要同时集成buggly和我们自己的这个处理器,怎么办呢?先注册buggly,后注册我们自己的处理器即可。
代码执行逻辑如下:先把buggly的处理器保存到mDefaultHandler
中,等我们自己的处理器执行完成后,再调用mDefaultHandler
的uncaughtException()
方法,执行buggly的逻辑。
Report 类
/**
* Created by shusheng007
*/
public class Report {
private Date time;
private String versionName;
private String osVersion;
private String deviceBrand;
private long usedMemory;
private long availableHeepSize;
private String errorMessage;
private String invokeStackInfo;
private DateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private Report(Builder builder) {
this.time = builder.time;
this.versionName = builder.versionName;
this.osVersion = builder.osVersion;
this.deviceBrand = builder.deviceBrand;
this.usedMemory = builder.usedMemory;
this.availableHeepSize = builder.availableHeepSize;
this.errorMessage = builder.errorMessage;
this.invokeStackInfo = builder.invokeStackInfo;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\n\n------------------------------crash begin---------------------------------\n\n");
sb.append("time: " + mDateFormat.format(time));
sb.append("\n");
sb.append("versionName: " + versionName);
sb.append("\n");
sb.append("osVersion: " + osVersion);
sb.append("\n");
sb.append("deviceBrand: " + deviceBrand);
sb.append("\n");
sb.append("usedMemory: " + usedMemory + "MB");
sb.append("\n");
sb.append("availableHeepSize: " + availableHeepSize + "MB");
sb.append("\n");
sb.append("errorMessage: " + errorMessage);
sb.append("\n");
sb.append("invokeStackInfo:\n" + invokeStackInfo);
sb.append("\n\n-------------------------------crash end-----------------------------------\n\n");
return sb.toString();
}
public static class Builder {
private Date time;
private String versionName;
private String osVersion;
private String deviceBrand;
private long usedMemory;
private long availableHeepSize;
private String errorMessage;
private String invokeStackInfo;
public Builder(Date time) {
this.time = time;
}
public Builder setVersionName(String versionName) {
this.versionName = versionName;
return this;
}
public Builder setOsVersion(String osVersion) {
this.osVersion = osVersion;
return this;
}
public Builder setDeviceBrand(String deviceBrand) {
this.deviceBrand = deviceBrand;
return this;
}
public Builder setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
return this;
}
public Builder setInvokeStackInfo(String invokeStackInfo) {
this.invokeStackInfo = invokeStackInfo;
return this;
}
public Builder setUsedMemory(long usedMemory) {
this.usedMemory = usedMemory;
return this;
}
public Builder setAvailableHeepSize(long availableHeepSize) {
this.availableHeepSize = availableHeepSize;
return this;
}
public Report build() {
return new Report(this);
}
}
}
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
Future<Boolean> future = mService.submit(() -> {
save2File(reportLocation, generateReport(throwable).toString() + "\n\n");
return true;
});
try {
if (future.get().booleanValue()) {
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, throwable);
} else {
android.os.Process.killProcess(android.os.Process.myPid());
}
}
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, e.getMessage(), e);
}
使用
在你App 的application 类里面初始化即可
UncaughtExceptionHandler .getInstance().init(this);
然后到你指定的目录里面去查看崩溃报告。
总结
明天就是国庆节了,是中华人民共和国成立70周年纪念日,祝伟大的祖国繁荣昌盛,人民安居乐业。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/14743.html