CrashHandler处理异常

情景

Android应用无法避免不会发生crash,可能属于系统底层的bug、或者机型适配、亦或者糟糕的网络环境。
当crash发生时,系统会kill我们的应用程序,会闪退或者提升用户程序已停止运行,而且更恐怖的是,对于开发者而言,是无法知道当时用户所操作或者面临的网络环境的,也无能为力的去解决改bug。
但是Android系统提供了这类问题的方法:UncaughtExceptionHandler

CrashHandler

public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

源码给出的定义如下:当一个线程由于未捕获异常而即将终止时,会回调这个接口。
当然有设置该接口的:

public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        // Android-removed: SecurityManager stubbed out on Android
        /*
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(
                new RuntimePermission("setDefaultUncaughtExceptionHandler")
                    );
        }
        */

         defaultUncaughtExceptionHandler = eh;
     }

思路

根据上面的理解,我们可以设置一个UncaughtExceptionHandler接口。当系统发送crash时,就会回调uncaughtException接口,这是可以普获到异常信息了。
我们可以记录当前的崩溃信息,存在本地,记得记录当时硬件环境。再下次合理的时机将错误信息上传,对于缓存问题,如果存在脏数据导致的经常性崩溃,就可以强行清除本地缓存,也是一种避险手段。在上传服务器有几种选择:一是立即上传;二是等待应用空闲上传。

public class CrashHandler implements UncaughtExceptionHandler {

    private static final String TAG = "CrashHandler";
    private static final String INT_CRASH_TIME = "int_crash_time";

    private static final int MAX_CRASH_TIME = 3;

    private static CrashHandler instance;
    private Context mContext;
    private UncaughtExceptionHandler mOriginalHandler;

    public static CrashHandler getInstance() {
        if (instance == null) {
            instance = new CrashHandler();
        }
        return instance;
    }

    /**
     * 初始化
     */
    public void init(Context context) {
        mContext = context;
        mOriginalHandler = Thread.getDefaultUncaughtExceptionHandler();
        /**
         * 若打开L开关则小翼不打盹,反之则打盹
         * */
//        if (!L.isDebug) {
        //设置该CrashHandler为程序的默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
//        }
    }

    /**
     * 把崩溃信息放在sd卡缓存目录下,若没有sd卡,就放在data/data中的缓存目录下
     *
     * <p>SDCard/Android/data/你的应用包名/cache/<p/>
     */
    private static File getDiskCrashDir(Context context, String dirName) {
        String cachePath;
        if (context.getExternalCacheDir() != null) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getFilesDir().getPath();
        }
        return new File(cachePath + File.separator + dirName);
    }

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        try {
            Log.e("未捕获的异常", ex.toString());
            handleCrash();
            if (BuildConfig.DEBUG) {
                saveCrash2File(ex);
            }
            uploadExceptionToService(ex);

        } catch (Exception e) {
            Log.e("uncaughtException又抛出的异常",  ex.toString());
        }
        mOriginalHandler.uncaughtException(thread, ex);
    }

    private void uploadExceptionToService(Throwable ex) {
        //上传操作
        // 一定要获取一些硬件信息(在不侵犯隐私的前提下)
    }

    private void handleCrash() {
        if (MyApplication.getInstance() != null) {
            //elapsedTime 这个是 系统启动后到发送崩溃的时间
            final long elapsedTime = SystemClock.elapsedRealtime() - MyApplication.getInstance().getStartTime();
            Log.e(TAG, "elapsedTime:" + elapsedTime);
            //如果在启动后10s内就崩溃了,那就需要清除缓存了,很大概率就是缓存出现了脏数据,导致启动读取缓存数据就崩溃了。
            if (elapsedTime < 10 * 1000) {
                int time = (Integer) SPUtil.get(MyApplication.getInstance(),"app",INT_CRASH_TIME, 0);
                time += 1;
                Log.e(TAG, "崩溃次数" + time);
                if (time >= MAX_CRASH_TIME) {
                    Log.e(TAG, "崩溃次数达到上限,清除缓存");
                    //reset
                    SPUtil.put(MyApplication.getInstance(),"app",INT_CRASH_TIME, 0);
                    //到达上限 清除缓存相关数据
                    CacheUtil.clean();
                } else {
                    SPUtil.put(MyApplication.getInstance(),"app",INT_CRASH_TIME, time);
                }
            }
        } else {
            Log.e(TAG, "获取不到context");
        }
    }

    /**
     * 保存错误信息到文件中
     *
     * @param ex 崩溃信息
     * @throws IOException
     */
    public void saveCrash2File(Throwable ex) throws IOException {

        StringBuilder sb = new StringBuilder();
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        ex.printStackTrace(printWriter);
        Throwable cause = ex.getCause(); // 返回此异常的原因(尝试加载类时发生错误引发的异常;否则返回 null)
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String result = writer.toString();
        sb.append(result);
        try {
            // 用于格式化日期,作为日志文件名的一部分
            DateFormat formatter = new SimpleDateFormat("MM.dd@HH.mm", Locale.CHINA);
            String time = formatter.format(new Date());
            String fileName = time + ".log";
            File dir = getDiskCrashDir(mContext, "crash");
            if (!dir.exists()) {
                if (!dir.mkdirs()) {
                    return;
                }
            }
            String filePath = dir.getAbsolutePath() + File.separator + fileName;
            FileOutputStream fos = new FileOutputStream(filePath);
            fos.write(sb.toString().getBytes());
            fos.close();
        } catch (IOException e) {
            Log.e(TAG,e.toString());
        }
    }

}

在应用启动的时候初始化: CrashHandler.getInstance().init(getApplication());