【版權申明】非商業目的可自由轉載
博文地址: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週年紀念日,祝偉大的祖國繁榮昌盛,人民安居樂業。