天天看點

Android如何捕獲應用的crash資訊

轉載請注明出處:http://blog.csdn.net/fishle123/article/details/50823358

我們的應用不可避免的會發生crash,如果是在調試階段,我們可以使用Logcat檢視異常資訊。但是如果應用釋出之後呢?如果在使用者那邊crash了,如果我們可以捕獲這些crash資訊,那麼對我們定位crash原因并修複問題是很有幫助的。應用crash即可能是Java層的異常導緻的,也可能是native層導緻,下面分别來看一下該如何處理。

1 Java層的未捕獲異常處理

先來看一下Java層的crash資訊收集吧。要想捕獲Java層的crash資訊并不難,Android已經提供了接口來幫助我們監控系統的未捕獲的異常:使用Thread.setDefaultUncaughtExceptionHandler就可以讓我們輕松的監控應用的任何意外crash。

首先來看一下Thread.setDefaultUncaughtExceptionHandler這個方法:

/**
 * Sets the default uncaught exception handler. This handler is invoked in
 * case any Thread dies due to an unhandled exception.
 *
 * @param handler
 *            The handler to set or null.
 */
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
    Thread.defaultUncaughtHandler = handler;
}
           

從Thread.setDefaultUncaughtExceptionHandler這個方法的注釋就可以看到:當程序内(因為Thread.defaultUncaughtHandler 是一個靜态變量,是以對整個程序内的所有線程有效)的任何線程發生未捕獲異常時,會調用這裡設定的handler。那我們看一下UncaughtExceptionHandler 這個類吧:

/**
 * Implemented by objects that want to handle cases where a thread is being
 * terminated by an uncaught exception. Upon such termination, the handler
 * is notified of the terminating thread and causal exception. If there is
 * no explicit handler set then the thread's group is the default handler.
 */
public static interface UncaughtExceptionHandler {
    /**
     * The thread is being terminated by an uncaught exception. Further
     * exceptions thrown in this method are prevent the remainder of the
     * method from executing, but are otherwise ignored.
     *
     * @param thread the thread that has an uncaught exception
     * @param ex the exception that was thrown
     */
    void uncaughtException(Thread thread, Throwable ex);
}
           

從源碼可以看出,UncaughtExceptionHandler 其實是一個接口,它隻定義了一個方法uncaughtException(Thread thread, Throwable ex),當線程因為遇到未捕獲異常而終止的時候就會調用這個方法。

如果我們想要捕獲應用的crash資訊,那麼定義一個自己的UncaughtExceptionHandler 就可以,當然我們需要在自己的UncaughtExceptionHandler 裡面把crash資訊儲存起來,必要的時候還可以上傳到我們的伺服器,這樣就可以很友善的收集使用者的crash資訊。

2 native層的異常處理

如果我們的應用使用到c/c++,那麼也需要收集native層的異常處理。大家都知道,Android的底層是基于Linux的,那麼native層的未捕獲異常就可以通過捕獲信号來處理了。Native層如果異常終止會發出SIGKILL信号,我們可以使用sigaaction來注冊一個信号處理函數來處理SIGKILL信号,這樣就可以收集到native層的未捕獲異常了。這裡給出一個大概的代碼架構:

void sigkill_handler(int signo){
   //列印堆棧,并寫入到檔案中
}
void install(){
    struct sigaction act, oldact;
    act.sa_handler = sigkill_handler;
    sigaddset(&act.sa_mask, SIGKILL);

    sigaction(SIGKILL, &act, &oldact);//注冊信号處理函數
    ......
}
           

3 實作

結合上面的介紹,下面就來定義一個自己的UncaughtExceptionHandler ,這個例子隻處理了Java層的crash收集,并把收集到的crash資訊儲存到sd卡上。這裡給我們自定義的crash處理器起了一個名字叫做AppCR(Application Crash Response)。

首先定義ErrorReporter ,它實作了UncaughtExceptionHandler :

public class ErrorReporter implements UncaughtExceptionHandler {

    private final Application mContext;
    private final ReporterExecutor mReporterExecutor;

    ErrorReporter(Application context, boolean enabled) {
        mContext = context;

        final Thread.UncaughtExceptionHandler defaultExceptionHandler = Thread
                .getDefaultUncaughtExceptionHandler();
        mReporterExecutor = new ReporterExecutor(context, defaultExceptionHandler);
        mReporterExecutor.setEnabled(enabled);
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    @Override
    public void uncaughtException(final Thread thread,final Throwable ex) {
        // TODO Auto-generated method stub
        LogUtil.i(AppCR.LOG_TAG,"catch uncaughtException");

        mReporterExecutor.execute(thread, ex);
    }

    public void setEnabled(boolean enabled) {
        LogUtil.i(AppCR.LOG_TAG, "AppCR is" + (enabled ? "enabled" : "disabled") + " for "
                + mContext.getPackageName());
    }
}
           

ReporterExecutor會調用Thread.setDefaultUncaughtExceptionHandler(this);來修改預設的UncaughtExceptionHandler。當發生未捕獲的異常時,調用mReporterExecutor.execute(thread, ex);來處理異常。

ReporterExecutor 中把異常資訊以及作業系統的相關資訊儲存到檔案中。

public class ReporterExecutor {

    public static final String TAG = ReporterExecutor.class.getSimpleName();
    private Context mContext;
    private boolean mEnabled = false;
    private final Thread.UncaughtExceptionHandler mDefaultExceptionHandler;
    private File mCrashInfoFile;

    public ReporterExecutor(Context context,
                            Thread.UncaughtExceptionHandler defaultedExceptionHandler) {

        mContext = context;
        mDefaultExceptionHandler = defaultedExceptionHandler;

        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            File path = Environment.getExternalStorageDirectory();
            File dir = new File(path, "BleFairy");
            if (!dir.exists()) {
                dir.mkdirs();
            }

            mCrashInfoFile = new File(dir, getCrashFileName());
            if (!mCrashInfoFile.exists()) {
                try {
                    mCrashInfoFile.createNewFile();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

    public boolean isEnabled() {
        return mEnabled;
    }

    public void setEnabled(boolean enabled) {
        mEnabled = enabled;
    }

    public void execute(Thread thread, Throwable ex) {

        if (!mEnabled) {
            endApplication(thread, ex);
            return;
        }

        // log crash info to file
        Log.w(AppCR.LOG_TAG, "getSysInfo.");
        CrashReportData data = CrashReportData.produce(thread, ex, mContext);
        data.writeToFile(mCrashInfoFile);
        endApplication(thread, ex);
       
    }

    private void endApplication(Thread thread, Throwable ex) {

        if (mDefaultExceptionHandler != null) {
            Log.w(AppCR.LOG_TAG, "execute default uncaughtException handler.");
            mDefaultExceptionHandler.uncaughtException(thread, ex);
        } else {
            Log.w(AppCR.LOG_TAG, "kill process and exit.");
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(10);
        }
    }

    private String getCrashFileName() {
        StringBuilder ret = new StringBuilder();
        Calendar calendar = Calendar.getInstance();

        ret.append("crash_");
        ret.append(calendar.get(Calendar.YEAR));
        int month = calendar.get(Calendar.MONTH)+1;
        int date = calendar.get(Calendar.DATE);
        if(month < 10 ){
            ret.append("0");
        }
        ret.append(month);
        if(date<10){
            ret.append("0");
        }
        ret.append(date);
        ret.append(".txt");
        return ret.toString();
    }
}
           

CrashReportData 類用于儲存異常資訊:

public class CrashReportData {

    private final String info;

    private CrashReportData(String crashInfo) {
        this.info = crashInfo;
    }

    public static CrashReportData produce(Thread thread, Throwable ex, Context context) {

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream print = new PrintStream(out);
        out.toString();

        print.append("crahtime:" + TimeUtil.getCurTimeString()).append("\n");
        print.append(SysInfo.getSysInfo(context)).append("\n");
        print.append(thread.getName()).append("(threadID=" + thread.getId() + ")").append("\n");
        print.append(ex.getMessage()).append("\n");
        ex.printStackTrace(print);

        return new CrashReportData(out.toString());
    }

    public void writeToFile(File file) {
        PrintWriter printer = null;
        try {

            // append to the end of crash file
            BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file, true));
            printer = new PrintWriter(out);
            printer.println(info);
            printer.flush();

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();

        } finally {

            if (printer != null) {
                printer.close();
            }
            LogUtil.w(AppCR.LOG_TAG, "write exception info to file over.");

        }
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return info;
        // return super.toString();
    }

}
           

 SysIno類:

public class SysInfo {

    public static String getSysInfo(Context context) {
        StringBuilder info = new StringBuilder();
        info.append("osVersion=Android ").append(Build.VERSION.RELEASE).append("\n");
        info.append("model=").append(Build.MODEL).append("\n");
        info.append("brand=").append(Build.BRAND).append("\n");

        LogUtil.i(AppCR.LOG_TAG, "sys info collect over.");
        return info.toString();
    }

}
           

使用AppCR來安裝我們的crash處理器:

public class AppCR {
    public static final String LOG_TAG=AppCR.class.getSimpleName();
    private static ErrorReporter mErrorReporter;

    public static void init(Application application){
        init(application,true);
    }

    public static void init(Application application,boolean enabled){
        mErrorReporter = new ErrorReporter(application, enabled);
    }
}
           

Application中安裝上面自定義的AppCR就可以了:

public class BeaconApplication extends Application {

    private final String TAG = "BeaconFairy.BeaconApplication";
    

    @Override
    public void onCreate() {
        super.onCreate();
        AppCR.init(this,true);
    }

}
           

需要注意的是:我們需要定義自己的Application,然後修改manifest就可以啦,還要記得加上寫SD卡的權限:

<application
        android:name=".BeaconApplication"
        android:allowBackup="true"
        android:allowTaskReparenting="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
         ........
</application>
           

申請寫SD卡的權限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
           

到此為止,我們自定義的crash資訊收集程式AppCR就完成了。