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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章