手把手擼一個實用必備CrashHandler

擼一個項目必備的CrashHandler

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


目錄

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

認識與做用

CrashHandler: 崩潰處理器,捕獲Crash信息並做出相應的處理java

  1. 測試使用:應用在平常的開發中,咱們常常須要去Logcat測試咱們的App,但因爲不少緣由,Android Monitor會閃屏或者Crash信息丟失。 這個時候就須要一個CrashHandler來將Crash寫入到本地方便咱們隨時隨地查看。
  2. 上線使用:應用的崩潰率是用戶衡量篩選應用的重要標準,那麼應用上線之後 咱們沒法向用戶藉手機來分析崩潰緣由。爲了減低崩潰率,這個時候須要CrashHandler 來幫咱們將崩潰信息返回給後臺,以便及時修復。

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

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); 會致使應用自動重啓三次,會影響一點用戶體驗github

因此咱們使用了一個土方法,就是讓它去打開一個咱們本身設定的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整個寫下來思路是三步 :
一、異常捕獲
二、信息數據採集
三、 數據寫入本地和上傳服務器
項目地址:Github地址
這是我一個隨便寫寫的項目
CrashHandler主要在rbase的util,還有app的MyApplication 中應用到服務器

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息