Android 收集Crash信息及用戶操作步驟

對于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異常,輸出里就可以看到想要的信息了


image.png

至此,可以說是已經(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ì)列


image.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載(漫談 iOS Crash 收集框架) 前言 很早以前就和念茜認(rèn)識,念茜不但技術(shù)功底扎實(shí),而且長得很漂亮,說她...
    狂風(fēng)無跡閱讀 3,584評論 1 11
  • 以下為文章正文,如果覺得有用,歡迎給她打賞。 為了能夠第一時間發(fā)現(xiàn)程序問題,應(yīng)用程序需要實(shí)現(xiàn)自己的崩潰日志收集服務(wù)...
    赤色追風(fēng)閱讀 2,614評論 1 11
  • 來源:程序媛念茜的博客 Crash日志收集 為了能夠第一時間發(fā)現(xiàn)程序問題,應(yīng)用程序需要實(shí)現(xiàn)自己的崩潰日志收集服務(wù),...
    幸福的魚閱讀 1,260評論 0 2
  • 比較好的轉(zhuǎn)載:http://www.cocoachina.com/ios/20151218/14748.html轉(zhuǎn)...
    liudhkk閱讀 988評論 0 2
  • 新的一年,給自己點(diǎn)壓力。 逼一下自己,要開始還房貸了。 我這種人有時候就是喜歡這樣,不逼一把自己,不知道為什么而活...
    夢鹿是一只貓閱讀 261評論 0 0

友情鏈接更多精彩內(nèi)容