Android收集程序崩溃日志

开个头

程序崩溃是我们开发人员最不想看到的,但也是我们不可避免的。在我们开发阶段,当程序发生崩溃的时候,我们需要根据打印的错误日志来定位,分析,解决错误。但是当我们把应用发布到应用市场的之后,用户使用我们应用的时候因为各种原因程序发生了崩溃,这个是非常影响用户体验的。这种情况下,我们无法知道是否发生了崩溃,更无法知道是什么地方,因为什么原因发生了崩溃。现在市场上也有一些第三方平台替我们做了这些事情,比如腾讯的Bugly,和友盟的统计等。但是我们怎样实现自己的统计呢?

首先我们先看下崩溃。
Android中崩溃分为两种,一种是Java代码崩溃,一种是Native代码崩溃。本篇只分析Java代码崩溃。

Java代码的崩溃

Java代码的崩溃,就是Java代码发生了异常。我们先看下Java的异常类。
这里写图片描述
这些Java的异常类,对于编译器来说,可以分为两大类:

unCheckedException(非检查异常):Error和RuntimeException以及他们各自的子类,都是非检查异常。换句话说,当我们编译程序的时候,编译器并不会提示我们这些异常。要么我们在编程的时候,对于可能抛出异常的代码加上try…catch,要么就等着运行的时候崩溃就好了。

checkedException(检查异常):除了UncheckedException之外,其他的都是checkedExcption。对于这种异常,我们的代码通常都无法进行编译,因为as都会提示我们出错了。这个时候要强制加上try…catch,或者将异常throw。

UncaughtExceptionHandler

了解了Java的异常类之后,我们再看一个关键类。UncaughtExceptionHandler

 /**
     * Interface for handlers invoked when a <tt>Thread</tt> abruptly
     * terminates due to an uncaught exception.
     * 处理接口,当一个线程由于未捕获的异常突然停止的时候调用。
     * 
     * <p>When a thread is about to terminate due to an uncaught exception
     * the Java Virtual Machine will query the thread for its
     * <tt>UncaughtExceptionHandler</tt> using
     * {@link #getUncaughtExceptionHandler} and will invoke the handler's
     * <tt>uncaughtException</tt> method, passing the thread and the
     * exception as arguments.
     * 当一个线程由于一个未捕获的异常即将崩溃的时候,Java虚拟机将会通过【getUncaughtExceptionHandler()】方法,来
     * 查询这个线程的【UncaughtExceptionHandler】,并且会调用他的【uncaughtException()】方法,并且把当前线程
     * 和异常作为参数传进去。
     * 
     * If a thread has not had its <tt>UncaughtExceptionHandler</tt>
     * explicitly set, then its <tt>ThreadGroup</tt> object acts as its
     * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
     * has no
     * special requirements for dealing with the exception, it can forward
     * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
     * default uncaught exception handler}.
     *如果一个线程没有设置他的【UncaughtExceptionHandler】,那么他的ThreadGroup对象就会作为他的
     *【UncaughtExceptionHandler】。如果【ThreadGroup】没有特殊的处理异常的需求,那么就会转调
     *【getDefaultUncaughtExceptionHandler】这个默认的处理异常的handler。
     *(线程组的东西我们先不管,我们只需要知道,如果Thread没有设置【UncaughtExceptionHandler】的话,那么
     *最终会调用【getDefaultUncaughtExceptionHandler】获取默认的【UncaughtExceptionHandler】来处理异常)
     *
     * @see #setDefaultUncaughtExceptionHandler
     * @see #setUncaughtExceptionHandler
     * @see ThreadGroup#uncaughtException
     * @since 1.5
     */
    @FunctionalInterface
    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.
         * 当传过来的【Thread】因为穿过来的未捕获的异常而停止时候调用这个方法。
         * 所有被这个方法抛出的异常,都将会被java虚拟机忽略。
         * 
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

这个类,准确的说,这个接口,其实就和我们收集崩溃日志有关系。
如果给一个线程设置了UncaughtExceptionHandler 这个接口:
1、这个线程中,所有未处理或者说未捕获的异常都将会由这个接口处理,也就说被这个接口给try…catch了。
2、在这个线程中抛出异常时,java虚拟机将会忽略,也就是说,java虚拟机不会让程序崩溃了。
3、如果没有设置,那么最终会调用getDefaultUncaughtExceptionHandler 获取默认的UncaughtExceptionHandler 来处理异常。

我们都知道我们的android程序是跑在UI线程中的,而且我们会在程序中创建各种子线程。为了统一,如果我们给每个线程都通过setUncaughtExceptionHandler() 这个方法来设置UncaughtExceptionHandler 的话,未免太不优雅了。在上面官方代码的注释中有一句,就是如果线程没有设置UncaughtExceptionHandler ,那么会通过getDefaultUncaughtExceptionHandler 来获取默认的UncaughtExceptionHandler 来处理异常。
这样的话,我们只需要在我们应用程序打开的时候,设置一个默认的UncaughtExceptionHandler ,就可以统一处理我们应用程序中所有的异常了!

talk is cheap,show me the code

首先自定义一个UncaughtExceptionHandler ,在 uncaughtException(Thread t, Throwable e) 方法中我们对抛出的异常进行处理,所谓的收集崩溃日志,就是把崩溃信息保存下来,等到合适的时机吧信息传到服务器上。不过一般选择保存信息的方法都是吧信息写入到磁盘里。代码中的逻辑也比较简单,也不做过多的解释。

public class MyCrashHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.e("程序出现异常了", "Thread = " + t.getName() + "\nThrowable = " + e.getMessage());
        String stackTraceInfo = getStackTraceInfo(e);
        Log.e("stackTraceInfo", stackTraceInfo);
        saveThrowableMessage(stackTraceInfo);
    }
   /**
     * 获取错误的信息
     *
     * @param throwable
     * @return
     */
    private String getStackTraceInfo(final Throwable throwable) {
        PrintWriter pw = null;
        Writer writer = new StringWriter();
        try {
            pw = new PrintWriter(writer);
            throwable.printStackTrace(pw);
        } catch (Exception e) {
            return "";
        } finally {
            if (pw != null) {
                pw.close();
            }
        }
        return writer.toString();
    }

    private String logFilePath = Environment.getExternalStorageDirectory() + File.separator + "Android" +
            File.separator + "data" + File.separator + MyApp.getInstance().getPackageName() + File.separator + "crashLog";

    private void saveThrowableMessage(String errorMessage) {
        if (TextUtils.isEmpty(errorMessage)) {
            return;
        }
        File file = new File(logFilePath);
        if (!file.exists()) {
            boolean mkdirs = file.mkdirs();
            if (mkdirs) {
                writeStringToFile(errorMessage, file);
            }
        } else {
            writeStringToFile(errorMessage, file);
        }
    }

    private void writeStringToFile(final String errorMessage, final File file) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                FileOutputStream outputStream = null;
                try {
                    ByteArrayInputStream inputStream = new ByteArrayInputStream(errorMessage.getBytes());
                    outputStream = new FileOutputStream(new File(file, System.currentTimeMillis() + ".txt"));
                    int len = 0;
                    byte[] bytes = new byte[1024];
                    while ((len = inputStream.read(bytes)) != -1) {
                        outputStream.write(bytes, 0, len);
                    }
                    outputStream.flush();
                    Log.e("程序出异常了", "写入本地文件成功:" + file.getAbsolutePath());
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (outputStream != null) {
                        try {
                            outputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

}

完成了我们的自定义UncaughtExceptionHandler ,接下来就是在我们程序启动的时候,把他设置为默认的就好了,一般是在application中设置。

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate(); 
        MyCrashHandler handler = new MyCrashHandler();
        Thread.setDefaultUncaughtExceptionHandler(handler);
    }
}

小试牛刀

开始写bug。。。

主线程报错

首先在我们的主线程搞一个空指针出来。

    private void testUIThreadException() {
        String string = null;
        char[] chars = string.toCharArray();
    }

然后运行程序。可以看到打印出来了Log,而且也成功写入了手机磁盘中。
这里写图片描述
这里写图片描述

子线程报错

然后在子线程搞一个ArithmeticException,也就是除数为0时,抛出的异常。

    private void testThreadException() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                int s = 10 / i;
            }
        });
        thread.start();
    }

再次运行。也是没有问题的!
这里写图片描述
这里写图片描述

注意

1、即使我们用这种方式捕获到了异常,保证程序不会闪退,如果是子线程出现了异常,那么还好,并不会影响UI线程的正常流程,但是如果是UI线程中出现了异常,那么程序就不会继续往下走,处于没有响应的状态,所以,我们处理异常的时候,应该给用户一个有好的提示,让程序优雅地退出。
2、Thread.setDefaultUncaughtExceptionHandler(handler)方法,如果多次调用的话,会以最后一次调用时,传递的handler为准,之前设置的handler都没用。所以,这也是如果用了第三方的统计模块后,可能会出现失灵的情况。(这种情况其实也好解决,就是只设置一个handler,以这handler为主,然后在这个handler的uncaughtException 方法中,调用其他的handler的uncaughtException 方法,保证都会收到异常信息)

如有错误,欢迎指正~

猜你喜欢

转载自blog.csdn.net/xy4_android/article/details/80846610