對于android開發(fā)者來說,Crash 可謂是司空見慣的事了,沒有一個程序員敢保證自己的程序絕對不會發(fā)生crash。開發(fā)的時候發(fā)生crash還好,可以Logcat來查看log分析出原因,但是在線上,用戶使用過程中發(fā)生crash的話,可能就沒法復(fù)現(xiàn)不知道原因了。所以異常信息的收集就顯得猶為重要。
收集crash信息,可以接入第三方的crash收集分析平臺,如bugtags、bugly等,參照對應(yīng)文檔接入來使用就好。當(dāng)然也可以自己簡單地來實(shí)現(xiàn)。
若是自己來實(shí)現(xiàn),Android中是有提供處理異常問題的方法的:
Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)
作用就是設(shè)置一個未捕獲的異常處理handler, 這個handler可以在任意線程被任意未處理的異常喚醒。
在實(shí)現(xiàn)UncaughtExceptionHandler之前,先來簡單了解一下Exception的知識。
Android中Exception可以分為兩類,檢查型異常(Checked Exception)和非檢查型異常(Unchecked Exception),Checked異常繼承java.lang.Exception類。Unchecked異常繼承自java.lang.RuntimeException類。
檢查型異常:異常必須被顯式地捕獲或者傳遞。通俗地講是指編譯器會檢查這一類異常,這類異常的發(fā)生難于避免,所以必須處理,如果不處理,編譯器一般會給出錯誤提示,而且不處理也不能通過編譯。如FileNotFoundException、ClassNotFoundException
非檢查型異常:可以不必做捕獲或拋出處理。這類異常可能是程序邏輯本身有問題,比如空對象NullPointerException、數(shù)組越界IndexOutOfBoundsException,這些是可以避免,所以編譯器不會強(qiáng)制檢查。
UncaughtExceptionHandler只能收到那些沒有被捕獲的異常,代碼中的catch異常是不會交給UncaughtExceptionHandler處理的
使用UncaughtExceptionHandler
程序crash可能發(fā)生在任意線程中,主線程不可以捕獲到子線程的Exception。UncaughtExceptionHandler可以給某個單獨(dú)的線程來設(shè)置
currentThread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler())
也可以設(shè)置所有線程都捕獲
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler())
實(shí)現(xiàn)自己的UncaughtExceptionHandler
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mUncaughtHandler;
CrashHandler() {
mUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler(); //系統(tǒng)的UncaughtExceptionHandler
}
@Override
public void uncaughtException(Thread t, Throwable e) {
//在這里處理異常信息
System.out.printf("\n手機(jī)品牌:%s", DeviceUtil.getDeviceBrand());
System.out.printf("\n手機(jī)型號:%s", DeviceUtil.getSystemModel());
System.out.printf("\n系統(tǒng)型號:%s\n\n", DeviceUtil.getSystemVersion());
Writer eInfo = new StringWriter();
PrintWriter printWriter = new PrintWriter(eInfo);
e.printStackTrace(printWriter);
System.out.printf("%s\n", eInfo.toString());
// 殺死進(jìn)程
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
新建一個類來實(shí)現(xiàn)Thread.UncaughtExceptionHandler接口,然后重寫uncaughtException方法就可以了。
當(dāng)有未處理的異常發(fā)生的時候,系統(tǒng)便會回調(diào)UncaughtExceptionHandler中的uncaughtException方法,并把異常發(fā)生所在線程,以及異常信息帶過來。在這里開發(fā)者可以自己來處理異常信息,可以把異常的堆棧信息保存到本地,或者是上報到服務(wù)端。處理完異常信息,也可以進(jìn)行一些異常提示操作,如彈窗提示用戶程序發(fā)生的崩潰,然后再進(jìn)行退出操作,這樣用戶的體驗(yàn)也會好一點(diǎn)。
有時候我們可能是想自己處理異常之后系統(tǒng)再繼續(xù)執(zhí)行原本異常處理操作,不想完全覆蓋掉系統(tǒng)的KillApplicationHandler。這也是可以的,只需要把先前存的系統(tǒng)的UncaughtHandler,放在最后調(diào)用就可以了
mUncaughtHandler.uncaughtException(t, e);
代碼里模擬一個NullPointerException異常,輸出里就可以看到想要的信息了

至此,可以說是已經(jīng)完成了獲取crash信息的這一步了,就是這么簡單
記錄用戶操作步驟
除了crash信息外,知道用戶的操作步驟,也可以給開發(fā)者們查找crash原因提供一定的的幫助。所以我們也可以把用戶的操作步驟,在哪個頁面,當(dāng)前什么生命周期,點(diǎn)了哪些view,這些一并記錄下來。
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ExceptionMonitor.getInstance().onCreate(this);
}
@Override
protected void onResume() {
super.onResume();
ExceptionMonitor.getInstance().onResume(this);
}
@Override
protected void onPause() {
super.onPause();
ExceptionMonitor.getInstance().onPause(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ExceptionMonitor.getInstance().onDestroy(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
ExceptionMonitor.getInstance().onDispatchTouchEvent(this, ev);
return super.dispatchTouchEvent(ev);
}
}
記錄操作步驟的關(guān)鍵是需要一個BaseActivity,在BaseActivity里記錄每個界面的進(jìn)入退出,以及事件分發(fā)情況。把每個頁面的進(jìn)入退出情況同步記錄到一個隊(duì)列里邊
private static SequenceQueue<String> stepQueue = new SequenceQueue<>(); //儲存用戶操作步驟的隊(duì)列
private static StringBuilder stepBuilder = new StringBuilder();
public static void enqueueStep(Context context, String state) {
String time = TIME_FORMATTER.format(System.currentTimeMillis());
stepBuilder.append(time)
.append(" ")
.append(context.getClass().getName())
.append(" ")
.append(state);
String s = stepBuilder.toString();
stepQueue.add(s);
stepBuilder.delete(0, stepBuilder.length());
}
public static void enqueueStep(Context context, View view) {
if (view == null) { return; }
String time = TIME_FORMATTER.format(System.currentTimeMillis());
String path = view.getResources().getResourceName(view.getId());
String viewId = path.substring(path.indexOf("/") + 1);
stepBuilder.append(time)
.append(" ")
.append(context.getClass().getName())
.append(" Event:viewId:")
.append(viewId)
.append(" Type: ")
.append(view.getClass().getName());
String s = stepBuilder.toString();
stepQueue.add(s);
stepBuilder.delete(0, stepBuilder.length());
}
public static String flushString() {
String s = stepQueue.flushString();
stepQueue.clear();
return s;
}
下面來重點(diǎn)看一下事件這一塊,從這一塊里可以得到對應(yīng)view的id。
public void onDispatchTouchEvent(Activity activity, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
this.lastX = event.getRawX();
this.lastY = event.getRawY();
} else if(event.getAction() == MotionEvent.ACTION_UP) {
float currentX = event.getRawX();
float currentY = event.getRawY();
if (lastX == currentX && lastY == currentY) {
// 判斷是點(diǎn)擊操作
View view = this.getView(activity.getWindow().getDecorView(), currentX, currentY);
// 把view事件記錄到隊(duì)列里邊
StepsHelper.enqueueStep(activity, view);
}
}
}
private View getView(View decorView, float currentX, float currentY) {
View targetView = null;
int[] pos = new int[2];
decorView.getLocationInWindow(pos);
if (determinePos(currentX, currentY, pos[0], pos[1], decorView.getWidth(), decorView.getHeight())) {
if (decorView instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup)decorView).getChildCount(); ++i) {
View tempView = ((ViewGroup) decorView).getChildAt(i);
// 遞歸獲取目標(biāo)view
targetView = getView(tempView, currentX, currentY);
if (targetView != null) {
break;
}
}
} else {
targetView = decorView;
}
}
return targetView;
}
// 判斷觸控點(diǎn)在窗口范圍內(nèi)
private boolean determinePos(float var1, float var2, int var3, int var4, int var5, int var6) {
return var1 >= (float)var3 && var1 <= (float)(var3 + var5) && var2 >= (float)var4 && var2 <= (float)(var4 + var6);
}
在獲取Event后,可以使用當(dāng)前的activity的decorView來判斷位置以及是否是ViewGroup來算出當(dāng)前是哪個view,有了view便可通用R文件來獲取它在項(xiàng)目的id了。
flushString輸出一下操作記錄隊(duì)列

大功告成,現(xiàn)在crash信息有了,用戶的操作步驟也有了,當(dāng)然還可以更加地完善,把更多的信息如手機(jī)內(nèi)存情況,網(wǎng)絡(luò)情況等記錄下來,然后還可以上報到服務(wù)端,這樣線上用戶的崩潰信息你也能看到了。