Android異常捕獲篇(上)---限制大小存儲到本地

前言

前段時間公司提個需求要做異常捕獲,本來爲了方便是想集成第三方的sdk,但是因爲我們的產品比較特殊,對比了友盟、testin、騰訊、阿里都不太符合公司需求,沒辦法只好自己做了。上網找了一些案例開始造輪子了~

思路

1、首先要實現Thread.UncaughtExceptionHandler接口,當出現crash報錯時程序會通過回調uncaughtException方法進行日誌的抓取
2、將抓取的日誌進行存儲到本地
3、根據自己的需求,將一些需要的信息同日志信息同時存儲都本地
4、爲了避免緩存日誌過多佔用不必要的內存,我們可以做一個限制緩存大小,比如本地只緩存十條信息,超出十條就將最舊的數據刪除

關鍵模塊示例

1、初始化集合用來存儲需要的字段

/**
 * 保存異常日誌信息集合
 */
private LinkedHashMap<String, String> crashAppLog = new LinkedHashMap<String, String>();

2、正常在收集日誌的時候肯定是要知道異常發生的時間,所以存儲時間是必不可少的

private void readTimer(){
        //獲取寫入當前時間
        curtTimer = ""+System.currentTimeMillis();

        if (formate == null) {

            formate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

        }

        timer = formate.format(new Date());

        crashAppLog.put("time", timer);
    }

3、根據自己的需求存儲一些設備信息或應用信息

/**
     * 獲取包相關信息
     * @param mContext
     * @throws PackageManager.NameNotFoundException
     */
    private void readPACKAGE_INFO(Context mContext) throws PackageManager.NameNotFoundException {

        PackageManager packageManager = mContext.getPackageManager();

        if (packageManager != null) {

            PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);

            if (packageInfo != null) {

                String versionName = packageInfo.versionName;

                String versionCode = ""+packageInfo.versionCode;

                String packName = packageInfo.packageName;

                crashAppLog.put("version_name",versionName);
            }
        }
    }

4、關鍵部分,寫入捕獲的日誌

/**
     * 寫入文件中
     * @param ex
     */
    private void writerCrashLogToFile(Throwable ex) {

        try {

            StringBuffer buffer = new StringBuffer();

            if (crashAppLog != null && crashAppLog.size() >0) {

                for (Map.Entry<String, String> entry:crashAppLog.entrySet()) {

                    buffer.append(entry.getKey()+":"+entry.getValue()+"\n");

                }
            }

            Writer writer = new StringWriter();

            PrintWriter printWriter = new PrintWriter(writer);
           //e.printStackTrace()是打印異常的堆棧信息,由內向外層層捕獲,所以下面需要while依次遍歷
            ex.printStackTrace(printWriter);

            Throwable cause = ex.getCause();

            while(cause != null) {

                cause.printStackTrace(printWriter);

                cause = cause.getCause();
            }

            printWriter.flush();

            printWriter.close();

            String result = writer.toString();

            buffer.append("Exception_log:\n");

            buffer.append(result);

            writerToFile(buffer.toString(), timer, curtTimer);

        }catch (Exception e) {
            Log.e(TAG, "writerCrashLogToFile - "+e.getMessage());
        }
    }

5、爲了防止寫入和讀取上傳同時操作一份文件,這裏做了一個重命名處理,上傳的地方加一下過濾

private void writerToFile(String s, String timer, String curtTimer) {

        try {
            /**
             * 創建日誌文件名稱
             *
             * 此處爲了防止讀寫同時發生,寫完以後重命名
             */
            String fileName = "write-"+timer+"-"+curtTimer+".log";

            String refileName = "crash-"+timer+"-"+curtTimer+".log";

            /**
             * 創建文件夾
             */
            File folder = new File(CAHCE_CRASH_LOG);

            if (!folder.exists())
                folder.mkdirs();

            /**
             * 創建日誌文件
             */
            File file = new File(folder.getAbsolutePath()+File.separator+fileName);

            if (!file.exists())
                file.createNewFile();

            FileWriter fileWriter = new FileWriter(file);

            BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

            bufferedWriter.write(s);

            bufferedWriter.flush();

            file.renameTo(new File(folder.getAbsolutePath()+File.separator+refileName));

            bufferedWriter.close();

            //sendCrashLogToServer(folder, file);//此處另開一個app負責上傳文件

        }catch (Exception e) {
            Log.e(TAG, "writerToFile - "+e.getMessage());
        }
    }

5、限制存儲文件數量與過濾排序

/**
     * 最大文件數量與排序
     * @param limitLogCount
     */
    private void limitAppLogCount(int limitLogCount) {

        try {

            File file = new File(CAHCE_CRASH_LOG);

            if (file != null && file.isDirectory()) {

                File[] files = file.listFiles(new CrashLogFliter());

                if(files != null && files.length >0) {

                    Arrays.sort(files, comparator);

                    if (files.length > LIMIT_LOG_COUNT) {

                        for (int i = 0 ; i < files.length - LIMIT_LOG_COUNT ;i++) {

                            files[i].delete();
                        }
                    }

                }
            }

        }catch (Exception e) {
            Log.e(TAG, "limitAppLogCount - "+e.getMessage());
        }
    }

完整代碼

此類貼出時臨時刪除了自己公司採集的設備信息,如有誤刪可以查看下上下邏輯即可,應該是可以直接使用的

/**
 * 實現bug收集存儲、上傳的抽象類
 */
public abstract class CrashUtil implements Thread.UncaughtExceptionHandler{

    private static final String TAG = "CrashUtil";

    /**
     * 允許最大日誌文件的數量
     */
    private int LIMIT_LOG_COUNT = 10;

    /**
     * 簡單日期格式
     */
    private SimpleDateFormat formate = null;

    /**
     * 保存異常日誌信息集合
     */
    private LinkedHashMap<String, String> crashAppLog = new LinkedHashMap<String, String>();
    /**
     * 默認放在內存卡的root路徑
     */
    private String CAHCE_CRASH_LOG = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator;
    /**
     * 系統默認的異常類
     */
    private Thread.UncaughtExceptionHandler mUncaughtExceptionHandler;
    /**
     * 日誌寫入時間
     */
    private String curtTimer;
    private String timer;

    /**
     * 抽象方法,
     * 在該類初始化的時候使用
     */
    public abstract void initParams(CrashUtil crashUtil);

    /**
     * 發送一場日誌文件到服務器
     * @param folder 文件路徑
     * @param file 文件
     */
    public abstract void sendCrashLogToServer(File folder, File file);

    /**
     * 上下文參數
     */
    private Context mContext;

    public void init(Context context) {

       try {
           if (context == null)
               throw new NullPointerException("Application 的Context不能爲空");

           this.mContext = context;

           /**
            * 獲取系統默認的異常處理類
            */
           mUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();

           /**
            * 在獲取系統異常類之前子類可以先做一些初始化的操作
            */
           initParams(this);
           /**
            * 使用當前的類爲異常處理類
            */
           Thread.setDefaultUncaughtExceptionHandler(this);
       }catch (Exception e){
           Log.e(TAG, "init - "+e.getMessage());
       }

    }


    /**
     * 此類是當應用出現異常的時候執行該方法
     * @param thread
     * @param throwable
     */
    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {

        try {

            if (!hanlderException(throwable) && mUncaughtExceptionHandler != null) {

                /**
                 * 如果此異常不處理則由系統自己處理
                 */
                this.mUncaughtExceptionHandler.uncaughtException(thread, throwable);

            }else{

                /**
                 * 可以延遲一秒鐘在退出
                 */
//                Thread.sleep(1000);

                android.os.Process.killProcess(android.os.Process.myPid());
                System.exit(1);

            }
        }catch (Exception e) {
            Log.e(TAG, "uncaughtException - "+e.getMessage());
        }
    }

    /**
     * 用戶處理異常日誌
     * @param throwable
     * @return
     */
    private boolean hanlderException(Throwable throwable) {

        try {

            if (throwable == null)
                return false;

            new Thread(new Runnable() {
                @Override
                public void run() {

                    Looper.prepare();
                    Toast.makeText(mContext, "程序崩潰", Toast.LENGTH_SHORT).show();
                    Looper.loop();
                }
            }).start();

            /**
             * 收集應用信息
             */
            collectCrashLogInfo(mContext);
            /**
             * 將日誌寫入文件
             */
            writerCrashLogToFile(throwable);

            /**
             * 限制日子志文件的數量
             */
            limitAppLogCount(LIMIT_LOG_COUNT);

        } catch (Exception e) {
            Log.e(TAG, "hanlderException - " + e.getMessage());
        }
        return false;
    }

    /**
     * 最大文件數量與排序
     * @param limitLogCount
     */
    private void limitAppLogCount(int limitLogCount) {

        try {

            File file = new File(CAHCE_CRASH_LOG);

            if (file != null && file.isDirectory()) {

                File[] files = file.listFiles(new CrashLogFliter());

                if(files != null && files.length >0) {

                    Arrays.sort(files, comparator);

                    if (files.length > LIMIT_LOG_COUNT) {

                        for (int i = 0 ; i < files.length - LIMIT_LOG_COUNT ;i++) {

                            files[i].delete();
                        }
                    }

                }
            }

        }catch (Exception e) {
            Log.e(TAG, "limitAppLogCount - "+e.getMessage());
        }
    }


    /**
     * 日誌文件按修改時間排序
     */
    private Comparator<File> comparator = new Comparator<File>() {
        @Override
        public int compare(File l, File r) {

            if (l.lastModified() > r.lastModified())
                return 1;
            if (l.lastModified() < r.lastModified())
                return -1;

            return 0;
        }
    };

    /**
     * 過濾.log的文件
     */
    public class CrashLogFliter implements FileFilter {

        @Override
        public boolean accept(File file) {

            if (file.getName().endsWith(".log"))
                return true;

            return false;
        }
    }



    /**
     * 寫入文件中
     * @param ex
     */
    private void writerCrashLogToFile(Throwable ex) {

        try {

            StringBuffer buffer = new StringBuffer();

            if (crashAppLog != null && crashAppLog.size() >0) {

                for (Map.Entry<String, String> entry:crashAppLog.entrySet()) {

                    buffer.append(entry.getKey()+":"+entry.getValue()+"\n");

                }
            }

            Writer writer = new StringWriter();

            PrintWriter printWriter = new PrintWriter(writer);
           //e.printStackTrace()是打印異常的堆棧信息,由內向外層層捕獲,所以下面需要while依次遍歷
            ex.printStackTrace(printWriter);

            Throwable cause = ex.getCause();

            while(cause != null) {

                cause.printStackTrace(printWriter);

                cause = cause.getCause();
            }

            printWriter.flush();

            printWriter.close();

            String result = writer.toString();

            buffer.append("Exception_log:\n");

            buffer.append(result);

            writerToFile(buffer.toString(), timer, curtTimer);

        }catch (Exception e) {
            Log.e(TAG, "writerCrashLogToFile - "+e.getMessage());
        }
    }

    private void writerToFile(String s, String timer, String curtTimer) {

        try {
            /**
             * 創建日誌文件名稱
             *
             * 此處爲了防止讀寫同時發生,寫完以後重命名
             */
            String fileName = "write-"+timer+"-"+curtTimer+".log";

            String refileName = "crash-"+timer+"-"+curtTimer+".log";

            /**
             * 創建文件夾
             */
            File folder = new File(CAHCE_CRASH_LOG);

            if (!folder.exists())
                folder.mkdirs();

            /**
             * 創建日誌文件
             */
            File file = new File(folder.getAbsolutePath()+File.separator+fileName);

            if (!file.exists())
                file.createNewFile();

            FileWriter fileWriter = new FileWriter(file);

            BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

            bufferedWriter.write(s);

            bufferedWriter.flush();

            file.renameTo(new File(folder.getAbsolutePath()+File.separator+refileName));

            bufferedWriter.close();

            //sendCrashLogToServer(folder, file);//此處另開一個app負責上傳文件

        }catch (Exception e) {
            Log.e(TAG, "writerToFile - "+e.getMessage());
        }
    }

    /**
     * 獲取應用信息
     * @param mContext
     */
    private void collectCrashLogInfo(Context mContext) {

        try {
            if (mContext == null)
                return ;

            readTimer();

            readPACKAGE_INFO(mContext);

        }catch (Exception e) {
            Log.e(TAG, "collectDeviceInfo - "+e.getMessage());
        }
    }

    private void readTimer(){
        //獲取寫入當前時間
        curtTimer = ""+System.currentTimeMillis();

        if (formate == null) {

            formate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

        }

        timer = formate.format(new Date());

        crashAppLog.put("time", timer);
    }

    /**
     * 獲取包相關信息
     * @param mContext
     * @throws PackageManager.NameNotFoundException
     */
    private void readPACKAGE_INFO(Context mContext) throws PackageManager.NameNotFoundException {

        PackageManager packageManager = mContext.getPackageManager();

        if (packageManager != null) {

            PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);

            if (packageInfo != null) {

                String versionName = packageInfo.versionName;

                String versionCode = ""+packageInfo.versionCode;

                String packName = packageInfo.packageName;

                crashAppLog.put("version_name",versionName);
            }
        }
    }


    /**
     * 去除設備號文件中的空格和回車
     * @param str
     * @return
     */
    public String stringUtils(String str){

        String dest = "";

        if (str != null) {

            Pattern p = Pattern.compile("\\s*|\n");

            Matcher m = p.matcher(str);

            dest = m.replaceAll("");

            return dest;

        }
        return null;
    }

    public void getDEVICE_INFO(){

        //手機型號:android.os.Build.MODEL;
        //系統版本:android.os.Build.VERSION.SDK;
        //Android版本(SDK):android.os.Build.VERSION.RELEASE;

        try {

            Field[] fields = Build.class.getFields();

            if (fields != null && fields.length > 0) {

                for (Field field:fields) {

                    if (field != null) {

                        field.setAccessible(true);

                            //crashAppLog.put(field.getName(), field.get(null).toString());


                    }
                }
            }

        } catch (Exception e) {
            Log.e(TAG, "getDEVICE_INFO - "+e.getMessage());
        }

    }

    public void setLIMIT_LOG_COUNT(int LIMIT_LOG_COUNT) {
        this.LIMIT_LOG_COUNT = LIMIT_LOG_COUNT;
    }

    public void setCAHCE_CRASH_LOG(String CAHCE_CRASH_LOG) {
        this.CAHCE_CRASH_LOG = CAHCE_CRASH_LOG;
    }

}

提供了一個對外暴露接口的類,方便初始化也可以動態設置日誌存儲路徑和存儲大小以及上傳接口

/**
 * 動態設置存儲路徑、限制文件大小
 */
public class CrashUtilhandler extends CrashUtil {

    private String TAG = "CrashUtilhandler";

    public static CrashUtilhandler mCrashUtilhandler = null;

    private CrashUtilhandler(){};
    public static CrashUtilhandler getInstance() {

        if (mCrashUtilhandler == null)

            mCrashUtilhandler = new CrashUtilhandler();

        return mCrashUtilhandler;

    }

    @Override
    public void initParams(CrashUtil crashUtil) {

        if (crashUtil != null){

            crashUtil.setCAHCE_CRASH_LOG(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"crashlogs");

            crashUtil.setLIMIT_LOG_COUNT(10);

        }
    }

    @Override
    public void sendCrashLogToServer(File folder, File file) {
            Log.e(TAG, "文件夾:"+folder.getAbsolutePath()+" - "+file.getAbsolutePath()+"");
    }
}

OK!有了這兩個類,還差最後一個初始化調用就可以將日誌信息存儲到本地了

//最好是放在工程的Application裏初始化
CrashApphandler.getInstance().init(this);

還有寫入權限千萬不要忘記加了~

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

看一下存儲效果

time:2017-07-14-11-39-03
version_name:5.6.10
Exception_log:
java.lang.IndexOutOfBoundsException: Invalid index 1, size is 1
    at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:251)
    at java.util.ArrayList.get(ArrayList.java:304)
    at com.garea.launcher.login.LauncherLogin.onClick(LauncherLogin.java:447)
    at android.view.View.performClick(View.java:3511)
    at android.view.View$PerformClick.run(View.java:14105)
    at android.os.Handler.handleCallback(Handler.java:605)
    at android.os.Handler.dispatchMessage(Handler.java:92)
    at android.os.Looper.loop(Looper.java:137)
    at android.app.ActivityThread.main(ActivityThread.java:4424)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:511)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
    at dalvik.system.NativeStart.main(Native Method)

代碼裏有上傳接口,直接使用也可以,不過公司採用了另起一個apk專門用來上傳本地存儲crash日誌的方式,下一篇再講解採用retrofit上傳本地crash日誌,今天就到這裏吧~

發佈了41 篇原創文章 · 獲贊 42 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章