程序崩溃是我们开发人员最不想看到的,但也是我们不可避免的。在我们开发阶段,当程序发生崩溃的时候,我们需要根据打印的错误日志来定位,分析,解决错误。但是当我们把应用发布到应用市场的之后,用户使用我们应用的时候因为各种原因程序发生了崩溃,这个是非常影响用户体验的。这种情况下,我们无法知道是否发生了崩溃,更无法知道是什么地方,因为什么原因发生了崩溃。现在市场上也有一些第三方平台替我们做了这些事情,比如腾讯的Bugly,和友盟的统计等。但是我们怎样实现自己的统计呢?
首先我们先看下崩溃。
Android中崩溃分为两种,一种是Java代码崩溃,一种是Native代码崩溃。本篇只分析Java代码崩溃。
Java代码的崩溃,就是Java代码发生了异常。我们先看下Java的异常类。
这些Java的异常类,对于编译器来说,可以分为两大类:
unCheckedException(非检查异常):Error和RuntimeException以及他们各自的子类,都是非检查异常。换句话说,当我们编译程序的时候,编译器并不会提示我们这些异常。要么我们在编程的时候,对于可能抛出异常的代码加上try…catch,要么就等着运行的时候崩溃就好了。
checkedException(检查异常):除了UncheckedException之外,其他的都是checkedExcption。对于这种异常,我们的代码通常都无法进行编译,因为as都会提示我们出错了。这个时候要强制加上try…catch,或者将异常throw。
了解了Java的异常类之后,我们再看一个关键类。UncaughtExceptionHandler
/**
* Interface for handlers invoked when a Thread abruptly
* terminates due to an uncaught exception.
* 处理接口,当一个线程由于未捕获的异常突然停止的时候调用。
*
* When a thread is about to terminate due to an uncaught exception
* the Java Virtual Machine will query the thread for its
* UncaughtExceptionHandler using
* {@link #getUncaughtExceptionHandler} and will invoke the handler's
* uncaughtException method, passing the thread and the
* exception as arguments.
* 当一个线程由于一个未捕获的异常即将崩溃的时候,Java虚拟机将会通过【getUncaughtExceptionHandler()】方法,来
* 查询这个线程的【UncaughtExceptionHandler】,并且会调用他的【uncaughtException()】方法,并且把当前线程
* 和异常作为参数传进去。
*
* If a thread has not had its UncaughtExceptionHandler
* explicitly set, then its ThreadGroup object acts as its
* UncaughtExceptionHandler. If the ThreadGroup 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.
* 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 ,就可以统一处理我们应用程序中所有的异常了!
首先自定义一个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 方法,保证都会收到异常信息)
如有错误,欢迎指正~