手把手擼一個實用必備CrashHandler

Rayhahah發表於2017-06-04

擼一個專案必備的CrashHandler

上週工作中新來的小夥伴問了一下專案中CrashHandler,當時只是簡單講了一下
週末到了,心血來潮,手把手擼一個好用全面的CrashHandler吧,對於以後專案開發和當前專案的完善也有一定的幫助。


目錄

  • 認識與作用
  • Crash的捕獲
  • Crash資訊的獲取
  • Crash日誌寫入上傳
  • 使用方式
  • 一些注意
  • 最後

認識與作用

CrashHandler: 崩潰處理器,捕獲Crash資訊並作出相應的處理

  1. 測試使用:應用在日常的開發中,我們經常需要去Logcat測試我們的App,但由於很多原因,Android Monitor會閃屏或者Crash資訊丟失。 這個時候就需要一個CrashHandler來將Crash寫入到本地方便我們隨時隨地檢視。
  2. 上線使用:應用的崩潰率是使用者衡量篩選應用的重要標準,那麼應用上線以後 我們無法向使用者藉手機來分析崩潰原因。為了減低崩潰率,這個時候需要CrashHandler 來幫我們將崩潰資訊返回給後臺,以便及時修復。

下面我們就手把手寫一個實用本地化輕量級的CrashHandler吧。

Crash的捕獲

  1. 實現Thread.UncaughtExceptionHandler介面,並重寫uncaughtException方法,此時你的CrashHandler就具備了接收處理異常的能力了。
  2. 呼叫Thread.setDefaultUncaughtExceptionHandler(CrashHandler) ,來使用我們自定義的CrashHandler來取代系統預設的CrashHandler
  3. 結合單例模式
  4. 總體三步: 捕獲異常、資訊資料獲取、資料寫入和上傳
    總體的初始化程式碼如下:

    private RCrashHandler(String dirPath) {
        mDirPath = dirPath;
        File mDirectory = new File(mDirPath);
        if (!mDirectory.exists()) {
            mDirectory.mkdirs();
        }
    }

    public static RCrashHandler getInstance(String dirPath) {
        if (INSTANCE == null) {
            synchronized (RCrashHandler.class) {
                if (INSTANCE == null) {
                    INSTANCE = new RCrashHandler(dirPath);
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 初始化
     *
     * @param context       上下文
     * @param crashUploader 崩潰資訊上傳介面回撥
     */
    public void init(Context context, CrashUploader crashUploader) {
        mCrashUploader = crashUploader;
        mContext = context;
        //儲存一份系統預設的CrashHandler
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        //使用我們自定義的異常處理器替換程式預設的
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 這個是最關鍵的函式,當程式中有未被捕獲的異常,系統將會自動呼叫uncaughtException方法
     *
     * @param t 出現未捕獲異常的執行緒
     * @param e 未捕獲的異常,有了這個ex,我們就可以得到異常資訊
     */
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        if (!catchCrashException(e) && mDefaultHandler != null) {
            //沒有自定義的CrashHandler的時候就呼叫系統預設的異常處理方式
            mDefaultHandler.uncaughtException(t, e);
        } else {
            //退出應用
            killProcess();
        }
    }

/**
     * 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成.
     *
     * @param ex
     * @return true:如果處理了該異常資訊;否則返回false.
     */
    private boolean catchCrashException(Throwable ex) {
        if (ex == null) {
            return false;
        }

        new Thread() {
            public void run() {
//                Looper.prepare();
//                Toast.makeText(mContext, "很抱歉,程式出現異常,即將退出", 0).show();
//                Looper.loop();
                Intent intent = new Intent();
                intent.setClass(mContext, CrashActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                ActivityCollector.finishAll();
                mContext.startActivity(intent);
            }
        }.start();
        //收集裝置引數資訊
        collectInfos(mContext, ex);
        //儲存日誌檔案
        saveCrashInfo2File();
        //上傳崩潰資訊
        uploadCrashMessage(infos);

        return true;
    }


  /**
     * 退出應用
     */
    public static void killProcess() {
        //結束應用
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                ToastUtils.showLong("哎呀,程式發生異常啦...");
                Looper.loop();
            }
        }).start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            RLog.e("CrashHandler.InterruptedException--->" + ex.toString());
        }
        //退出程式
        Process.killProcess(Process.myPid());
        System.exit(1);
    }複製程式碼

Crash資訊的獲取

  • 獲取異常資訊
 /**
     * 獲取捕獲異常的資訊
     *
     * @param ex
     */
    private String collectExceptionInfos(Throwable ex) {
        Writer mWriter = new StringWriter();
        PrintWriter mPrintWriter = new PrintWriter(mWriter);
        ex.printStackTrace(mPrintWriter);
        ex.printStackTrace();
        Throwable mThrowable = ex.getCause();
        // 迭代棧佇列把所有的異常資訊寫入writer中
        while (mThrowable != null) {
            mThrowable.printStackTrace(mPrintWriter);
            // 換行 每個個異常棧之間換行
            mPrintWriter.append("\r\n");
            mThrowable = mThrowable.getCause();
        }
        // 記得關閉
        mPrintWriter.close();
        return mWriter.toString();
    }複製程式碼
  • 獲取應用資訊
 /**
     * 獲取應用包引數資訊
     */
    private void collectPackageInfos(Context context) {
        try {
            // 獲得包管理器
            PackageManager mPackageManager = context.getPackageManager();
            // 得到該應用的資訊,即主Activity
            PackageInfo mPackageInfo = mPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
            if (mPackageInfo != null) {
                String versionName = mPackageInfo.versionName == null ? "null" : mPackageInfo.versionName;
                String versionCode = mPackageInfo.versionCode + "";
                mPackageInfos.put(VERSION_NAME, versionName);
                mPackageInfos.put(VERSION_CODE, versionCode);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }複製程式碼
  • 獲取裝置硬體資訊(針對不同機型的使用者更有效地定位Bug)
/**
     * 從系統屬性中提取裝置硬體和版本資訊
     */
    private void collectBuildInfos() {
        // 反射機制
        Field[] mFields = Build.class.getDeclaredFields();
        // 迭代Build的欄位key-value 此處的資訊主要是為了在伺服器端手機各種版本手機報錯的原因
        for (Field field : mFields) {
            try {
                field.setAccessible(true);
                mDeviceInfos.put(field.getName(), field.get("").toString());
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }複製程式碼
  • 獲取系統常規資訊(針對不同設定的使用者更有效定位Bug)
 /**
     * 獲取系統常規設定屬性
     */
    private void collectSystemInfos() {
        Field[] fields = Settings.System.class.getFields();
        for (Field field : fields) {
            if (!field.isAnnotationPresent(Deprecated.class)
                    && field.getType() == String.class) {
                try {
                    String value = Settings.System.getString(mContext.getContentResolver(), (String) field.get(null));
                    if (value != null) {
                        mSystemInfos.put(field.getName(), value);
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }複製程式碼
  • 獲取安全設定資訊
 /**
     * 獲取系統安全設定資訊
     */
    private void collectSecureInfos() {
        Field[] fields = Settings.Secure.class.getFields();
        for (Field field : fields) {
            if (!field.isAnnotationPresent(Deprecated.class)
                    && field.getType() == String.class
                    && field.getName().startsWith("WIFI_AP")) {
                try {
                    String value = Settings.Secure.getString(mContext.getContentResolver(), (String) field.get(null));
                    if (value != null) {
                        mSecureInfos.put(field.getName(), value);
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }複製程式碼
  • 獲取應用記憶體資訊(需要許可權)
/**
     * 獲取記憶體資訊
     */
    private String collectMemInfos() {
        BufferedReader br = null;
        StringBuffer sb = new StringBuffer();

        ArrayList<String> commandLine = new ArrayList<>();
        commandLine.add("dumpsys");
        commandLine.add("meminfo");
        commandLine.add(Integer.toString(Process.myPid()));
        try {
            java.lang.Process process = Runtime.getRuntime()
                    .exec(commandLine.toArray(new String[commandLine.size()]));
            br = new BufferedReader(new InputStreamReader(process.getInputStream()), 8192);

            while (true) {
                String line = br.readLine();
                if (line == null) {
                    break;
                }
                sb.append(line);
                sb.append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }複製程式碼
  • 最後將這些資訊儲存到infos中,以便之後我們回傳給上傳具體功能時候更加方便,有了這些資料,我們應該能夠快速定位崩潰的原因了
 /**
     * 獲取裝置引數資訊
     *
     * @param context
     */
    private void collectInfos(Context context, Throwable ex) {
        mExceptionInfos = collectExceptionInfos(ex);
        collectPackageInfos(context);
        collectBuildInfos();
        collectSystemInfos();
        collectSecureInfos();
        mMemInfos = collectMemInfos();

        //將資訊儲存到一個總的Map中提供給上傳動作回撥
        infos.put(EXCEPETION_INFOS_STRING, mExceptionInfos);
        infos.put(PACKAGE_INFOS_MAP, mPackageInfos);
        infos.put(BUILD_INFOS_MAP, mDeviceInfos);
        infos.put(SYSTEM_INFOS_MAP, mSystemInfos);
        infos.put(SECURE_INFOS_MAP, mSecureInfos);
        infos.put(MEMORY_INFOS_STRING, mMemInfos);
    }複製程式碼

Crash日誌寫入

  1. 將崩潰資料寫入到本地檔案中(這裡我只收集了異常資訊應用資訊,具體情況可以根據自己需求來拼接其他資料)
 /**
     * 將崩潰日誌資訊寫入本地檔案
     */
    private String saveCrashInfo2File() {
        StringBuffer mStringBuffer = getInfosStr(mPackageInfos);
        mStringBuffer.append(mExceptionInfos);
        // 儲存檔案,設定檔名
        String mTime = formatter.format(new Date());
        String mFileName = "CrashLog-" + mTime + ".log";
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            try {
                File mDirectory = new File(mDirPath);
                Log.v(TAG, mDirectory.toString());
                if (!mDirectory.exists())
                    mDirectory.mkdirs();
                FileOutputStream mFileOutputStream = new FileOutputStream(mDirectory + File.separator + mFileName);
                mFileOutputStream.write(mStringBuffer.toString().getBytes());
                mFileOutputStream.close();
                return mFileName;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

 /**
     * 將HashMap遍歷轉換成StringBuffer
     */
    @NonNull
    public static StringBuffer getInfosStr(ConcurrentHashMap<String, String> infos) {
        StringBuffer mStringBuffer = new StringBuffer();
        for (Map.Entry<String, String> entry : infos.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            mStringBuffer.append(key + "=" + value + "\r\n");
        }
        return mStringBuffer;
    }複製程式碼
  1. 由於每一個應用上傳的伺服器或者資料型別都不同,所以為了更好的延展性,我們使用介面回撥,將獲取的全部資料拋給應用具體去實現(我這裡為了演示,使用了Bmob後端雲)




 /**
     * 上傳崩潰資訊到伺服器
     */
    public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
        mCrashUploader.uploadCrashMessage(infos);
    }

 /**
     * 崩潰資訊上傳介面回撥
     */
    public interface CrashUploader {
        void uploadCrashMessage(ConcurrentHashMap<String, Object> infos);
    }複製程式碼

使用方式

 /**
     * 初始化崩潰處理器
     */
    private void initCrashHandler() {

        mCrashUploader = new RCrashHandler.CrashUploader() {
            @Override
            public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
                CrashMessage cm = new CrashMessage();
                ConcurrentHashMap<String, String> packageInfos = (ConcurrentHashMap<String, String>) infos.get(RCrashHandler.PACKAGE_INFOS_MAP);
                cm.setDate(DateTimeUitl.getCurrentWithFormate(DateTimeUitl.sysDateFormate));
                cm.setVersionName(packageInfos.get(RCrashHandler.VERSION_NAME));
                cm.setVersionCode(packageInfos.get(RCrashHandler.VERSION_CODE));
                cm.setExceptionInfos(((String) infos.get(RCrashHandler.EXCEPETION_INFOS_STRING)));
                cm.setMemoryInfos((String) infos.get(RCrashHandler.MEMORY_INFOS_STRING));
                cm.setDeviceInfos(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
                        .get(RCrashHandler.BUILD_INFOS_MAP)).toString());
                cm.setSystemInfoss(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
                        .get(RCrashHandler.SYSTEM_INFOS_MAP)).toString());
                cm.setSecureInfos(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
                        .get(RCrashHandler.SECURE_INFOS_MAP)).toString());
                cm.save(new SaveListener<String>() {
                    @Override
                    public void done(String s, BmobException e) {
                        if (e == null) {
                            RLog.e("上傳成功!");

                        } else {
                            RLog.e("上傳Bmob失敗 錯誤碼:" + e.getErrorCode());
                        }
                    }
                });
            }
        };
        RCrashHandler.getInstance(FileUtils.getRootFilePath() + "EasySport/crashLog")
                .init(mAppContext, mCrashUploader);
    }複製程式碼

一些注意

使用過程中發現在Activity中 Process.killProcess(Process.myPid());System.exit(1); 會導致應用自動重啟三次,會影響一點使用者體驗

所以我們使用了一個土方法,就是讓它去開啟一個我們自己設定的CrashActivity來提高我們應用的使用者體驗

  /**
     * 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成.
     *
     * @param ex
     * @return true:如果處理了該異常資訊;否則返回false.
     */
    private boolean catchCrashException(Throwable ex) {
        if (ex == null) {
            return false;
        }

//啟動我們自定義的頁面
        new Thread() {
            public void run() {
//                Looper.prepare();
//                Toast.makeText(mContext, "很抱歉,程式出現異常,即將退出", 0).show();
//                Looper.loop();
                Intent intent = new Intent();
                intent.setClass(mContext, CrashActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                ActivityCollector.finishAll();
                mContext.startActivity(intent);
            }
        }.start();

        //收集裝置引數資訊
        collectInfos(mContext, ex);
        //儲存日誌檔案
        saveCrashInfo2File();
        //上傳崩潰資訊
        uploadCrashMessage(infos);

        return true;
    }複製程式碼

CrashActivity的話就看個人需求了,可以使一段Sorry的文字或者一些互動的反饋操作都是可以的。

最後

CrashHandler整個寫下來思路是三步 :
1、異常捕獲
2、資訊資料採集
3、 資料寫入本地和上傳伺服器
專案地址:Github地址
這是我一個隨便寫寫的專案
CrashHandler主要在rbase的util,還有app的MyApplication 中應用到

相關文章