未处理异常处理器 UncaughtExceptionHandler 实现 崩溃日志保存 与 重启应用

前言

当我们编写程序的时候 , 遇到会抛出异常的方法的时候 , 我们一般会采取 try … catch 的方式:

try {
        bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        }

但是万一我们捕捉不到的时候 , 程序就会FC , 弹出 某某某应用程序已停止运行 的对话框. 然后用户在受到打击之后还要重新点开应用 这从两点来说都是很不好的 , 也是这就我们这次要实现的功能

  • 当APP在用户使用的时候 , 我们无法处理得到的异常
  • 应用在崩溃之后没有响应的处理

说个题外的小技巧

手动获取日志的方法 (需要ROOT权限)
/data/system/dropbox 下存有所有应用的异常日志文件 , 非常时候可以手动提取

生成的日志文件:

未处理异常处理器 UncaughtExceptionHandler 实现 崩溃日志保存 与 重启应用_第1张图片

崩溃后的提示:

未处理异常处理器 UncaughtExceptionHandler 实现 崩溃日志保存 与 重启应用_第2张图片

重启后显示上次的异常日志:

未处理异常处理器 UncaughtExceptionHandler 实现 崩溃日志保存 与 重启应用_第3张图片

开始编写异常处理器

要使用到的一些参数

/**
 * Created by OCWVAR
 * Package: com.ocwvar.surfacetest.ExceptionHandler
 * Date: 2016/5/25  9:20
 * Project: SurfaceTest
 * 未处理异常接收器
 *
 *  在重新启动Activity时会传递数据包 Bundle
 *  也可直接使用  OCExceptionHandler.handleIncomingBundle()  方法进行处理
 *
 *  数据:
 *  IsRecover   .布尔类型.                          区别这个数据是否为崩溃重启的数据 永远为  true
 *  hasLogs     .布尔类型.                          是否成功生成了日志文件
 *  Throwable  .Serializable序列化类型.       上次崩溃的异常对象
 *
 *
 * 参数:
 * SLEEPTIME_RESTART_ACTIVITY        重新启动应用程序指定Activity间隔时间.  毫秒.  1000ms = 1s
 * RESTART_ACTIVITY                         重新启动的Activity类
 * LOG_NAME_HEAD                             日志文件名开头
 * LOG_SAVE_FOLDER                          日志保存目录
 * SAVE_LOGS                                    是否生成日志
 */
public class OCExceptionHandler{

    private final static long SLEEPTIME_RESTART_ACTIVITY = 2000;
    private final static Class RESTART_ACTIVITY = MainActivity.class;
    private final static String LOG_NAME_HEAD = "OCLog";
    private final static String LOG_SAVE_FOLDER = "/log/";
    private final static boolean SAVE_LOGS = true;

    public final static String THROWABLE_OBJECT = "Throwable";
    public final static String IS_RECOVERY = "IsRecover";
    public final static String HAS_LOGS = "hasLogs";

    private boolean logsCreated = false;

    ...
}

1.引用 Thread.UncaughtExceptionHandler 接口 继承 Application

(继承Application只是为了设置 setDefaultUncaughtExceptionHandler , 和使用 ApplicationContext 而已 , 如果可以有其他实现方式可以不继承)


这是就是我们一开始最基础的配置

public class OCExceptionHandler extends Application implements Thread.UncaughtExceptionHandler {

@Override
    public void onCreate() {
        super.onCreate();
        //设置捕捉全局未处理异常为我们这个类
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

@Override
    public void uncaughtException(Thread thread, Throwable ex){
        ...
    }

2.在程序崩溃之后 , 启动对应的Activity

/**
     * 重新启动应用程序
     * @param activityClass 要启动的Activity
     */
    private void restartActivity(Class activityClass , Throwable throwable){

        //创建用于启动的 Intent , 与对应的数据
        Intent intent = new Intent(getApplicationContext(),activityClass);
        intent.putExtra("IsRecover",true);
        intent.putExtra("hasLogs",logsCreated);
        intent.putExtra("Throwable",throwable);

        PendingIntent pendingIntent = PendingIntent.getActivity(
                getApplicationContext(),
                0,
                intent,
                PendingIntent.FLAG_ONE_SHOT
        );

        //获取闹钟管理器 , 用于定时执行我们的启动任务
        AlarmManager mgr = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
        //设置执行PendingIntent的时间是当前时间+SLEEPTIME_RESTART_ACTIVITY 参数的值
        mgr.set(AlarmManager.RTC, System.currentTimeMillis() + SLEEPTIME_RESTART_ACTIVITY , pendingIntent);
    }

3.在程序崩溃的时候记录下日志文件

/**
     * 创建日志文件
     * @param throwable 要记录的异常
     * @return  执行结果
     */
    private boolean createLogs(Throwable throwable){
        if (throwable == null || !SAVE_LOGS){
            return false;
        }

        if (Build.VERSION.SDK_INT >= 23 && !checkPermission()){
            Log.e("异常处理--保存日志", "保存失败 , Android 6.0+ 系统. 内存卡读写权限没有获取" );
            return false;
        }

        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            //如果内存卡或内置储存已经挂载

            //创建储存目录
            String savePath = Environment.getExternalStorageDirectory().getPath()+LOG_SAVE_FOLDER;
            File file = new File(savePath);
            file.mkdirs();

            if (file.canWrite()){
                //如果目录可以写入

                FileWriter fileWriter;
                PrintWriter printWriter;

                //得到当前的日期与时间 , 精确到秒
                String exceptionTime = DateFormat.format("yyyy-MM-dd hh:mm:ss", new Date()).toString();

                //创建日志文件对象
                file = new File(savePath + LOG_NAME_HEAD + "  " + exceptionTime + "  .log");
                try {
                    if (file.createNewFile()){
                    //如果文件创建成功 , 则写入文件
                        fileWriter = new FileWriter(file,true);
                        printWriter = new PrintWriter(fileWriter);
                        printWriter.println("Date:"+exceptionTime+"\n");
                        printWriter.println("Exception Class Name: ");
                        printWriter.println(throwable.getStackTrace()[0].getClassName());
                        printWriter.println("");
                        printWriter.println("Exception Class Position: ");
                        printWriter.println("Line number: "+throwable.getStackTrace()[0].getLineNumber());
                        printWriter.println("");
                        printWriter.println("Exception Cause: ");
                        printWriter.println(throwable.getMessage());
                        printWriter.println("");
                        printWriter.println("-----------------------------------\nException Message: \n");
                        for (int i = 0; i < throwable.getStackTrace().length; i++) {
                            printWriter.println(throwable.getStackTrace()[i]);
                        }
                        //清空与关闭用到的流
                        printWriter.flush();
                        fileWriter.flush();
                        printWriter.close();
                        fileWriter.close();
                        Log.w("异常处理--保存日志", "日志保存成功" );
                        return true;
                    }else {
                        Log.e("异常处理--保存日志", "保存失败 , 存在相同名称的日志文件" );
                        return false;
                    }
                } catch (IOException e) {
                    Log.e("异常处理--保存日志", "保存失败 , 无法创建日志文件或写入流失败" );
                    return false;
                }

            }else {
                //目录不可写入 , 操作失败
                Log.e("异常处理--保存日志", "保存失败 , 无法写入目录" );
                file = null;
                return false;
            }

        }else {
            Log.e("异常处理--保存日志", "保存失败 , 储存未挂载" );
            return false;
        }

    }

    /**
     * 检查内存卡读写权限 针对Android 6.0+
     * @return  是否有权限
     */
    @TargetApi(23)
    private boolean checkPermission(){
        return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    }

4.结尾工作 , 使用我们创建的方法

在方法public void uncaughtException(Thread thread, Throwable ex)中调用我们的方法

@Override
    public void uncaughtException(Thread thread, Throwable ex) {
        //记录日志生成结果 , 用于给Intent传递日志生存的结果
        logsCreated = createLogs(ex);
        restartActivity(RESTART_ACTIVITY , ex);
        //应用已经崩溃, 需要先终止当前的应用线程. 否则会ANR
        System.exit(2);
    }

然后在配置文件中使用我们这个类 (如果你是用你自己的Application类就不需要了):

"@mipmap/ic_launcher"
        android:label="TEST TEST TEST"
        android:theme="@style/AppTheme"

        //在这里进行注册Application类
        android:name=".ExceptionHandler.OCExceptionHandler">

        ...

        

注意

有的大佬可能会想在程序崩溃的时候显示一个Toast或对话框 , 但这是不行的 , 无论你是直接执行还是用Handler .我看了下Stack Overflow上的QA , 有个人提出的观点 , 也是我最认同的观点 :

当应用崩溃的时候 , 已经没有能用的ApplicationContext了 ,所以你用了之后都是没反应的.

但我发现如果你用的是Activity.Context就能达到目的 , 但是这会导致Activity无法被回收的风险 , 所以非常不建议这么用. 毕竟为了显示一句话而导致内存泄漏这就捡了芝麻丢了西瓜.

弥补缺点

我们既然不方便在 UncaughtExceptionHandler 里面进行提示 , 但我们既然会传 Intent 给启动的Activity , 那么我们直接用它来做就行了.

/**
     * 处理上次崩溃重启传回Activity的Bundle数据
     * @param bundle    传入的Bundle数据
     * @return  True: 处理成功  False:不是上次崩溃时传入的数据
     */
    public static boolean handleIncomingBundle(@NonNull Bundle bundle , Context context){

        //判断这个Bundle是不是我们崩溃后传回的数据
        if (bundle.get(IS_RECOVERY) != null){
            if (bundle.getBoolean(HAS_LOGS)){
                Toast.makeText(context, "程序已恢复 , 崩溃日志已生成", Toast.LENGTH_SHORT).show();
            }else {
                Toast.makeText(context, "程序已恢复", Toast.LENGTH_SHORT).show();
            }
            Throwable throwable = (Throwable)bundle.getSerializable(THROWABLE_OBJECT);
            if (throwable != null){
                Log.e("上次崩溃日志", "---------------------------------------------");
                for (int i = 0; i < throwable.getStackTrace().length; i++) {
                    System.out.println(throwable.getStackTrace()[i]);
                }
                Log.e("上次崩溃日志", "---------------------------------------------");
            }else {
                Log.e("上次崩溃日志", "日志丢失 或 记录失败 !");
            }
            return true;
        }else {
            return false;
        }
    }

在要启动的Activity中进行使用即可:

if (getIntent().getExtras() != null){
            OCExceptionHandler.handleIncomingBundle(getIntent().getExtras(),getApplicationContext());
        }

大佬们看完有啥要说的啊 ~~~

点个顶呗 QAQ

你可能感兴趣的:(Android)