1. 程式人生 > >未處理異常處理器 UncaughtExceptionHandler 實現 崩潰日誌儲存 與 重啟應用

未處理異常處理器 UncaughtExceptionHandler 實現 崩潰日誌儲存 與 重啟應用

前言

當我們編寫程式的時候 , 遇到會丟擲異常的方法的時候 , 我們一般會採取 try … catch 的方式:

try {
        bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        }

但是萬一我們捕捉不到的時候 , 程式就會FC , 彈出 某某某應用程式已停止執行 的對話方塊. 然後使用者在受到打擊之後還要重新點開應用 這從兩點來說都是很不好的 , 也是這就我們這次要實現的功能

  • 當APP在使用者使用的時候 , 我們無法處理得到的異常
  • 應用在崩潰之後沒有響應的處理

說個題外的小技巧

手動獲取日誌的方法 (需要ROOT許可權)
/data/system/dropbox 下存有所有應用的異常日誌檔案 , 非常時候可以手動提取

生成的日誌檔案:

這裡寫圖片描述

崩潰後的提示:

這裡寫圖片描述

重啟後顯示上次的異常日誌:

這裡寫圖片描述

開始編寫異常處理器

要使用到的一些引數

/**
 * Created by OCWVAR
 * Package: com.ocwvar.surfacetest.ExceptionHandler
 * Date: 2016/5/25  9:20
 * Project: SurfaceTest
 * 未處理異常接收器
 *
 *  在重新啟動Activity時會傳遞資料包 Bundle
 *  也可直接使用  OCExceptionHandler.handleIncomingBundle()  方法進行處理
 *
 *  資料:
 *  IsRecover   .布林型別.                          區別這個資料是否為崩潰重啟的資料 永遠為  true
 *  hasLogs     .布林型別.                          是否成功生成了日誌檔案
 *  Throwable  .Serializable序列化型別.       上次崩潰的異常物件
 *
 *
 * 引數:
 * SLEEPTIME_RESTART_ACTIVITY        重新啟動應用程式指定Activity間隔時間.  毫秒.  1000ms = 1s
 * RESTART_ACTIVITY                         重新啟動的Activity類
 * LOG_NAME_HEAD                             日誌檔名開頭
 * LOG_SAVE_FOLDER                          日誌儲存目錄
 * SAVE_LOGS                                    是否生成日誌
 */
public class OCExceptionHandler{ private final static long SLEEPTIME_RESTART_ACTIVITY = 2000; private final static Class RESTART_ACTIVITY = MainActivity.class; private final static String LOG_NAME_HEAD = "OCLog"; private final static String LOG_SAVE_FOLDER = "/log/"; private final static
boolean SAVE_LOGS = true; public final static String THROWABLE_OBJECT = "Throwable"; public final static String IS_RECOVERY = "IsRecover"; public final static String HAS_LOGS = "hasLogs"; private boolean logsCreated = false; ... }

1.引用 Thread.UncaughtExceptionHandler 介面 繼承 Application

(繼承Application只是為了設定 setDefaultUncaughtExceptionHandler , 和使用 ApplicationContext 而已 , 如果可以有其他實現方式可以不繼承)


這是就是我們一開始最基礎的配置

public class OCExceptionHandler extends Application implements Thread.UncaughtExceptionHandler {

@Override
    public void onCreate() {
        super.onCreate();
        //設定捕捉全域性未處理異常為我們這個類
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

@Override
    public void uncaughtException(Thread thread, Throwable ex){
        ...
    }

2.在程式崩潰之後 , 啟動對應的Activity

/**
     * 重新啟動應用程式
     * @param activityClass 要啟動的Activity
     */
    private void restartActivity(Class activityClass , Throwable throwable){

        //建立用於啟動的 Intent , 與對應的資料
        Intent intent = new Intent(getApplicationContext(),activityClass);
        intent.putExtra("IsRecover",true);
        intent.putExtra("hasLogs",logsCreated);
        intent.putExtra("Throwable",throwable);

        PendingIntent pendingIntent = PendingIntent.getActivity(
                getApplicationContext(),
                0,
                intent,
                PendingIntent.FLAG_ONE_SHOT
        );

        //獲取鬧鐘管理器 , 用於定時執行我們的啟動任務
        AlarmManager mgr = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
        //設定執行PendingIntent的時間是當前時間+SLEEPTIME_RESTART_ACTIVITY 引數的值
        mgr.set(AlarmManager.RTC, System.currentTimeMillis() + SLEEPTIME_RESTART_ACTIVITY , pendingIntent);
    }

3.在程式崩潰的時候記錄下日誌檔案

/**
     * 建立日誌檔案
     * @param throwable 要記錄的異常
     * @return  執行結果
     */
    private boolean createLogs(Throwable throwable){
        if (throwable == null || !SAVE_LOGS){
            return false;
        }

        if (Build.VERSION.SDK_INT >= 23 && !checkPermission()){
            Log.e("異常處理--儲存日誌", "儲存失敗 , Android 6.0+ 系統. 記憶體卡讀寫許可權沒有獲取" );
            return false;
        }

        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            //如果記憶體卡或內建儲存已經掛載

            //建立儲存目錄
            String savePath = Environment.getExternalStorageDirectory().getPath()+LOG_SAVE_FOLDER;
            File file = new File(savePath);
            file.mkdirs();

            if (file.canWrite()){
                //如果目錄可以寫入

                FileWriter fileWriter;
                PrintWriter printWriter;

                //得到當前的日期與時間 , 精確到秒
                String exceptionTime = DateFormat.format("yyyy-MM-dd hh:mm:ss", new Date()).toString();

                //建立日誌檔案物件
                file = new File(savePath + LOG_NAME_HEAD + "  " + exceptionTime + "  .log");
                try {
                    if (file.createNewFile()){
                    //如果檔案建立成功 , 則寫入檔案
                        fileWriter = new FileWriter(file,true);
                        printWriter = new PrintWriter(fileWriter);
                        printWriter.println("Date:"+exceptionTime+"\n");
                        printWriter.println("Exception Class Name: ");
                        printWriter.println(throwable.getStackTrace()[0].getClassName());
                        printWriter.println("");
                        printWriter.println("Exception Class Position: ");
                        printWriter.println("Line number: "+throwable.getStackTrace()[0].getLineNumber());
                        printWriter.println("");
                        printWriter.println("Exception Cause: ");
                        printWriter.println(throwable.getMessage());
                        printWriter.println("");
                        printWriter.println("-----------------------------------\nException Message: \n");
                        for (int i = 0; i < throwable.getStackTrace().length; i++) {
                            printWriter.println(throwable.getStackTrace()[i]);
                        }
                        //清空與關閉用到的流
                        printWriter.flush();
                        fileWriter.flush();
                        printWriter.close();
                        fileWriter.close();
                        Log.w("異常處理--儲存日誌", "日誌儲存成功" );
                        return true;
                    }else {
                        Log.e("異常處理--儲存日誌", "儲存失敗 , 存在相同名稱的日誌檔案" );
                        return false;
                    }
                } catch (IOException e) {
                    Log.e("異常處理--儲存日誌", "儲存失敗 , 無法建立日誌檔案或寫入流失敗" );
                    return false;
                }

            }else {
                //目錄不可寫入 , 操作失敗
                Log.e("異常處理--儲存日誌", "儲存失敗 , 無法寫入目錄" );
                file = null;
                return false;
            }

        }else {
            Log.e("異常處理--儲存日誌", "儲存失敗 , 儲存未掛載" );
            return false;
        }

    }

    /**
     * 檢查記憶體卡讀寫許可權 針對Android 6.0+
     * @return  是否有許可權
     */
    @TargetApi(23)
    private boolean checkPermission(){
        return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    }

4.結尾工作 , 使用我們建立的方法

在方法public void uncaughtException(Thread thread, Throwable ex)中呼叫我們的方法

@Override
    public void uncaughtException(Thread thread, Throwable ex) {
        //記錄日誌生成結果 , 用於給Intent傳遞日誌生存的結果
        logsCreated = createLogs(ex);
        restartActivity(RESTART_ACTIVITY , ex);
        //應用已經崩潰, 需要先終止當前的應用執行緒. 否則會ANR
        System.exit(2);
    }

然後在配置檔案中使用我們這個類 (如果你是用你自己的Application類就不需要了):

<application
        android:icon="@mipmap/ic_launcher"
        android:label="TEST TEST TEST"
        android:theme="@style/AppTheme"

        //在這裡進行註冊Application類
        android:name=".ExceptionHandler.OCExceptionHandler">

        ...

        </application>

注意

有的大佬可能會想在程式崩潰的時候顯示一個Toast或對話方塊 , 但這是不行的 , 無論你是直接執行還是用Handler .我看了下Stack Overflow上的QA , 有個人提出的觀點 , 也是我最認同的觀點 :

當應用崩潰的時候 , 已經沒有能用的ApplicationContext了 ,所以你用了之後都是沒反應的.

但我發現如果你用的是Activity.Context就能達到目的 , 但是這會導致Activity無法被回收的風險 , 所以非常不建議這麼用. 畢竟為了顯示一句話而導致記憶體洩漏這就撿了芝麻丟了西瓜.

彌補缺點

我們既然不方便在 UncaughtExceptionHandler 裡面進行提示 , 但我們既然會傳 Intent 給啟動的Activity , 那麼我們直接用它來做就行了.

/**
     * 處理上次崩潰重啟傳回Activity的Bundle資料
     * @param bundle    傳入的Bundle資料
     * @return  True: 處理成功  False:不是上次崩潰時傳入的資料
     */
    public static boolean handleIncomingBundle(@NonNull Bundle bundle , Context context){

        //判斷這個Bundle是不是我們崩潰後傳回的資料
        if (bundle.get(IS_RECOVERY) != null){
            if (bundle.getBoolean(HAS_LOGS)){
                Toast.makeText(context, "程式已恢復 , 崩潰日誌已生成", Toast.LENGTH_SHORT).show();
            }else {
                Toast.makeText(context, "程式已恢復", Toast.LENGTH_SHORT).show();
            }
            Throwable throwable = (Throwable)bundle.getSerializable(THROWABLE_OBJECT);
            if (throwable != null){
                Log.e("上次崩潰日誌", "---------------------------------------------");
                for (int i = 0; i < throwable.getStackTrace().length; i++) {
                    System.out.println(throwable.getStackTrace()[i]);
                }
                Log.e("上次崩潰日誌", "---------------------------------------------");
            }else {
                Log.e("上次崩潰日誌", "日誌丟失 或 記錄失敗 !");
            }
            return true;
        }else {
            return false;
        }
    }

在要啟動的Activity中進行使用即可:

if (getIntent().getExtras() != null){
            OCExceptionHandler.handleIncomingBundle(getIntent().getExtras(),getApplicationContext());
        }

大佬們看完有啥要說的啊 ~~~

點個頂唄 QAQ