Android中Crash收集

問題
Android應用不可避免會發生Crash,不管你的代碼寫得有多風騷,在這個複雜的網絡環境中,Crash還是時常的會發生。也就是常說的應用程序發生崩潰。常見表現就是閃屏然後退出。

原因
有些Crash是隻在特定網絡環境中才會出現,比如說網絡環境爲2g的時候。而Crash是發生在用戶手機端,如果我們不對這些異常信息進行收集,那我們就沒辦法分析出Crash的原因,也就無法進行修復。下面就介紹一下如何收集這些Crash信息。

問題分析:
首先,Crash是發生在客戶端,並且帶有不確定性,即有的用戶可能發生而又的不發生。所以,我們可以在應用程序發生Crash後,對Crash信息進行收集,保存在本地客戶端(如寫入的sd卡),然後在合適的時候(如網絡環境好)將Crash文件上傳到服務器進行統計分析。

技術實現:
Thread中提供了一個方法叫做setDefaultUncaughtUncaughtExceptionHandler(UncaughtExceptionHandler handler)的方法,用來設置一個UncaughtExceptionHandler對象。

這個對象有什麼用呢?

作用:當Crash發生的時候,系統就會回調UncaughtExceptionHandler 中的uncaughtException方法。所以我們就可以在這個方法中去保存crash日誌。然後在時機好的時候發送至服務器。

具體邏輯
下面代碼中,主要是創建了一個實現Thread.UncaughtExceptionHandler接口的類CrashHandler。
然後以單例的形式向外提供支持。

主要的方法有:

  • init (Context) 進行初始化
  • uncaughtException(Thread , Throwable ) 複寫的方法,每次Crash之後被調用
  • handleException(Throwable)處理Crash的邏輯
  • uploadExceptionToServer()上傳到服務器的實現了
  • dumpExceptionToSDCard(Throwable )保存到sd卡的實現類
  • dumpPhoneInfo(PrintWriter)獲取收集信息

實現的邏輯
在Application中調用init()方法初始化CrashHandler,當發生Crash的時候CrashHandler的uncaughtException方法被調用。然後在這個方法中調用handleException方法,handleException方法中再去組織具體邏輯(調用具體實現類)。uploadExceptionToServer、dumpExceptionToSDCard、dumpPhoneInfo這三個方法則是具體邏輯實現。

代碼實現:

  • CrashHandler 類:
public class CrashHandler implements Thread.UncaughtExceptionHandler {

    private static final String TAG = "CrashHandler";
    private static final boolean DEBUG = true;

    private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";

    private static final String FILE_NAME = "crash";
    private static final String FILE_NAME_SUFFIX = ".txt";

    private static CrashHandler sInstance = new CrashHandler();

    private Context mContext;

    //私有構造器
    private CrashHandler() {

    }

    //單例模式
    public static CrashHandler getInstance() {

        return sInstance;
    }

    //初始化
    public void init(Context context) {

        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context;

    }

    /**
     * 重寫uncaughtException
     * @param t 發生Crash的線程
     * @param ex Throwale對象
     */
    @Override
    public void uncaughtException(Thread t, Throwable ex) {
        //處理邏輯需要開啓一個子線程,用於文件的寫入操作
        handleException(ex);
        //在程序關閉之前休眠2秒,以避免在文件寫入的操作完
        //成。之前進程被殺死。
        //也可以考慮彈出對話框友好提示用戶
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.exit(0);
//        Process.killProcess(Process.myPid());
    }

    //處理異常
    private void handleException(final Throwable ex) {
        try {
            Executors.newSingleThreadExecutor().submit(new Runnable() {
                @Override
                public void run() {
                    dumpExceptionToSDCard(ex);
                    uploadExceptionToServer();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 將異常信息上傳至服務器
     */
    private void uploadExceptionToServer() {


    }

    /**
     * 將異常信息寫入sd卡
     * @param ex
     */
    private void dumpExceptionToSDCard(Throwable ex) {
        //判斷是否支持SD卡
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            if (DEBUG) {
                Log.i(TAG, "sdcard unfind ,skip dump exception");
                return;
            }
        }

        File dir = new File(PATH);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        long current = System.currentTimeMillis();
        String time = new SimpleDateFormat("yyyy-MM-dd").format(new Date(current));

        File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);

        try {
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));

            pw.println(time);
            dumpPhoneInfo(pw);
            pw.println();
            //將拋出的異常信息寫入到文件
            ex.printStackTrace(pw);
            pw.close();
        } catch (Exception e) {
            Log.d(TAG, "dump Exception Exception" + e.getMessage());
            e.printStackTrace();
        }

    }


    /**
     *
     * 獲取手機信息
     * @param pw 寫入流
     * @throws PackageManager.NameNotFoundException 異常
     */
    private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {

        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);

        pw.print("App Version : ");
        pw.print(pi.versionName);
        Log.d(TAG,"name : "+pi.versionName);
        pw.print('_');
        pw.println(pi.versionCode);

        pw.print("OS Version : ");
        pw.print(Build.VERSION.RELEASE);
        pw.print("_");
        pw.println(Build.VERSION.SDK_INT );

        pw.print("Vendor : ");
        pw.println(Build.MANUFACTURER);

        pw.print("Model : ");
        pw.println(Build.MODEL);
        pw.print("Cpu ABI : ");
        pw.println(Build.CPU_ABI);
    }

}
  • Application中初始化:
public class App extends Application {

    private  Context mContext;

    private static App sInstance;

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;

        CrashHandler crashHandler = CrashHandler.getInstance();
        crashHandler.init(this);

        mContext = getApplicationContext();

    }


    public  Context getContext() {
        return mContext;
    }

    public static App getsInstance() {
        return sInstance;
    }

}

大致實現就是這樣,主要是學習 Thread.UncaughtExceptionHandler和了解uncaughtException方法在進程Crash後會被調用。然後在這個基礎上去處理邏輯(重寫uncaughtException方法)。

後記
大家可以自己拋出一個異常,去測試一下是否成功。比如在Activity中使用以下代碼:

  throw new RuntimeException("自定義異常");

成功生成的文件內容:
文件內容

遇到的坑

  • 文件創建失敗,可能是文件的路徑不規範
  • 文件打開權限不足(Read Only),6.0以上需要動態權限申請
  • 開啓一個子線程去處理文件的讀寫,並且將當前線程sleep一定的時間。才能保證在文件寫完後,程序才被殺死。
  • 文件找不到,代碼中顯示已經生成,而使用DDMS查看或手機上自帶的文件管理程序查看的時候卻沒有。可以考慮直接使用adb shell查看或者使用RE文件管理器。

參考書籍

  • 《Android開發與藝術探索》
發佈了150 篇原創文章 · 獲贊 138 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章