1. 程式人生 > >Android收集程式崩潰日誌

Android收集程式崩潰日誌

開個頭

程式崩潰是我們開發人員最不想看到的,但也是我們不可避免的。在我們開發階段,當程式發生崩潰的時候,我們需要根據列印的錯誤日誌來定位,分析,解決錯誤。但是當我們把應用釋出到應用市場的之後,使用者使用我們應用的時候因為各種原因程式發生了崩潰,這個是非常影響使用者體驗的。這種情況下,我們無法知道是否發生了崩潰,更無法知道是什麼地方,因為什麼原因發生了崩潰。現在市場上也有一些第三方平臺替我們做了這些事情,比如騰訊的Bugly,和友盟的統計等。但是我們怎樣實現自己的統計呢?

首先我們先看下崩潰。
Android中崩潰分為兩種,一種是Java程式碼崩潰,一種是Native程式碼崩潰。本篇只分析Java程式碼崩潰。

Java程式碼的崩潰

Java程式碼的崩潰,就是Java程式碼發生了異常。我們先看下Java的異常類。
這裡寫圖片描述
這些Java的異常類,對於編譯器來說,可以分為兩大類:

unCheckedException(非檢查異常):Error和RuntimeException以及他們各自的子類,都是非檢查異常。換句話說,當我們編譯程式的時候,編譯器並不會提示我們這些異常。要麼我們在程式設計的時候,對於可能丟擲異常的程式碼加上try…catch,要麼就等著執行的時候崩潰就好了。

checkedException(檢查異常):除了UncheckedException之外,其他的都是checkedExcption。對於這種異常,我們的程式碼通常都無法進行編譯,因為as都會提示我們出錯了。這個時候要強制加上try…catch,或者將異常throw。

UncaughtExceptionHandler

瞭解了Java的異常類之後,我們再看一個關鍵類。UncaughtExceptionHandler

 /**
     * Interface for handlers invoked when a <tt>Thread</tt> abruptly
     * terminates due to an uncaught exception.
     * 處理介面,當一個執行緒由於未捕獲的異常突然停止的時候呼叫。
     * 
     * <p>When a thread is about to terminate due to an uncaught exception
     * the Java Virtual Machine will query the thread for its
     * <tt>UncaughtExceptionHandler</tt> using
     * {@link #getUncaughtExceptionHandler} and will invoke the handler's
     * <tt>uncaughtException</tt> method, passing the thread and the
     * exception as arguments.
     * 當一個執行緒由於一個未捕獲的異常即將崩潰的時候,Java虛擬機器將會通過【getUncaughtExceptionHandler()】方法,來
     * 查詢這個執行緒的【UncaughtExceptionHandler】,並且會呼叫他的【uncaughtException()】方法,並且把當前執行緒
     * 和異常作為引數傳進去。
     * 
     * If a thread has not had its <tt>UncaughtExceptionHandler</tt>
     * explicitly set, then its <tt>ThreadGroup</tt> object acts as its
     * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
     * has no
     * special requirements for dealing with the exception, it can forward
     * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
     * default uncaught exception handler}.
     *如果一個執行緒沒有設定他的【UncaughtExceptionHandler】,那麼他的ThreadGroup物件就會作為他的
     *【UncaughtExceptionHandler】。如果【ThreadGroup】沒有特殊的處理異常的需求,那麼就會轉調
     *【getDefaultUncaughtExceptionHandler】這個預設的處理異常的handler。
     *(執行緒組的東西我們先不管,我們只需要知道,如果Thread沒有設定【UncaughtExceptionHandler】的話,那麼
     *最終會呼叫【getDefaultUncaughtExceptionHandler】獲取預設的【UncaughtExceptionHandler】來處理異常)
     *
     * @see
#setDefaultUncaughtExceptionHandler * @see #setUncaughtExceptionHandler * @see ThreadGroup#uncaughtException * @since 1.5 */
@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. * 當傳過來的【Thread】因為穿過來的未捕獲的異常而停止時候呼叫這個方法。 * 所有被這個方法丟擲的異常,都將會被java虛擬機器忽略。 * * @param t the thread * @param e the exception */ void uncaughtException(Thread t, Throwable e); }

這個類,準確的說,這個介面,其實就和我們收集崩潰日誌有關係。
如果給一個執行緒設定了UncaughtExceptionHandler 這個介面:
1、這個執行緒中,所有未處理或者說未捕獲的異常都將會由這個介面處理,也就說被這個介面給try…catch了。
2、在這個執行緒中丟擲異常時,java虛擬機器將會忽略,也就是說,java虛擬機器不會讓程式崩潰了。
3、如果沒有設定,那麼最終會呼叫getDefaultUncaughtExceptionHandler 獲取預設的UncaughtExceptionHandler 來處理異常。

我們都知道我們的android程式是跑在UI執行緒中的,而且我們會在程式中建立各種子執行緒。為了統一,如果我們給每個執行緒都通過setUncaughtExceptionHandler() 這個方法來設定UncaughtExceptionHandler 的話,未免太不優雅了。在上面官方程式碼的註釋中有一句,就是如果執行緒沒有設定UncaughtExceptionHandler ,那麼會通過getDefaultUncaughtExceptionHandler 來獲取預設的UncaughtExceptionHandler 來處理異常。
這樣的話,我們只需要在我們應用程式開啟的時候,設定一個預設的UncaughtExceptionHandler ,就可以統一處理我們應用程式中所有的異常了!

talk is cheap,show me the code

首先自定義一個UncaughtExceptionHandler ,在 uncaughtException(Thread t, Throwable e) 方法中我們對丟擲的異常進行處理,所謂的收集崩潰日誌,就是把崩潰資訊儲存下來,等到合適的時機吧資訊傳到伺服器上。不過一般選擇儲存資訊的方法都是吧資訊寫入到磁盤裡。程式碼中的邏輯也比較簡單,也不做過多的解釋。

public class MyCrashHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.e("程式出現異常了", "Thread = " + t.getName() + "\nThrowable = " + e.getMessage());
        String stackTraceInfo = getStackTraceInfo(e);
        Log.e("stackTraceInfo", stackTraceInfo);
        saveThrowableMessage(stackTraceInfo);
    }
   /**
     * 獲取錯誤的資訊
     *
     * @param throwable
     * @return
     */
    private String getStackTraceInfo(final Throwable throwable) {
        PrintWriter pw = null;
        Writer writer = new StringWriter();
        try {
            pw = new PrintWriter(writer);
            throwable.printStackTrace(pw);
        } catch (Exception e) {
            return "";
        } finally {
            if (pw != null) {
                pw.close();
            }
        }
        return writer.toString();
    }

    private String logFilePath = Environment.getExternalStorageDirectory() + File.separator + "Android" +
            File.separator + "data" + File.separator + MyApp.getInstance().getPackageName() + File.separator + "crashLog";

    private void saveThrowableMessage(String errorMessage) {
        if (TextUtils.isEmpty(errorMessage)) {
            return;
        }
        File file = new File(logFilePath);
        if (!file.exists()) {
            boolean mkdirs = file.mkdirs();
            if (mkdirs) {
                writeStringToFile(errorMessage, file);
            }
        } else {
            writeStringToFile(errorMessage, file);
        }
    }

    private void writeStringToFile(final String errorMessage, final File file) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                FileOutputStream outputStream = null;
                try {
                    ByteArrayInputStream inputStream = new ByteArrayInputStream(errorMessage.getBytes());
                    outputStream = new FileOutputStream(new File(file, System.currentTimeMillis() + ".txt"));
                    int len = 0;
                    byte[] bytes = new byte[1024];
                    while ((len = inputStream.read(bytes)) != -1) {
                        outputStream.write(bytes, 0, len);
                    }
                    outputStream.flush();
                    Log.e("程式出異常了", "寫入本地檔案成功:" + file.getAbsolutePath());
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (outputStream != null) {
                        try {
                            outputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

}

完成了我們的自定義UncaughtExceptionHandler ,接下來就是在我們程式啟動的時候,把他設定為預設的就好了,一般是在application中設定。

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate(); 
        MyCrashHandler handler = new MyCrashHandler();
        Thread.setDefaultUncaughtExceptionHandler(handler);
    }
}

小試牛刀

開始寫bug。。。

主執行緒報錯

首先在我們的主執行緒搞一個空指標出來。

    private void testUIThreadException() {
        String string = null;
        char[] chars = string.toCharArray();
    }

然後執行程式。可以看到打印出來了Log,而且也成功寫入了手機磁碟中。
這裡寫圖片描述
這裡寫圖片描述

子執行緒報錯

然後在子執行緒搞一個ArithmeticException,也就是除數為0時,丟擲的異常。

    private void testThreadException() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                int s = 10 / i;
            }
        });
        thread.start();
    }

再次執行。也是沒有問題的!
這裡寫圖片描述
這裡寫圖片描述

注意

1、即使我們用這種方式捕獲到了異常,保證程式不會閃退,如果是子執行緒出現了異常,那麼還好,並不會影響UI執行緒的正常流程,但是如果是UI執行緒中出現了異常,那麼程式就不會繼續往下走,處於沒有響應的狀態,所以,我們處理異常的時候,應該給使用者一個有好的提示,讓程式優雅地退出。
2、Thread.setDefaultUncaughtExceptionHandler(handler)方法,如果多次呼叫的話,會以最後一次呼叫時,傳遞的handler為準,之前設定的handler都沒用。所以,這也是如果用了第三方的統計模組後,可能會出現失靈的情況。(這種情況其實也好解決,就是隻設定一個handler,以這handler為主,然後在這個handler的uncaughtException 方法中,呼叫其他的handler的uncaughtException 方法,保證都會收到異常資訊)

如有錯誤,歡迎指正~