我的GitHub | 我的博客 | 我的微信 | 我的邮箱 |
---|---|---|---|
baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
异常捕获
常用 API
setUncaughtExceptionHandler
public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
- 设置该线程由于未捕获到异常而突然终止时调用的处理程序。
- 通过明确设置未捕获到的异常处理程序,线程可以完全控制它对未捕获到的异常作出响应的方式。
- 如果没有设置这样的处理程序,则该线程的 ThreadGroup 对象将充当其处理程序。
public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()
- 返回该线程由于未捕获到异常而突然终止时调用的处理程序。
- 如果该线程尚未明确设置未捕获到的异常处理程序,则返回该线程的 ThreadGroup 对象,除非该线程已经终止,在这种情况下,将返回 null。
setDefaultUncaughtExceptionHandler
public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
- 设置当线程由于未捕获到异常而突然终止,并且没有为该线程定义其他处理程序时所调用的默认处理程序。
- 未捕获到的异常处理首先由线程控制,然后由线程的 ThreadGroup 对象控制,最后由未捕获到的默认异常处理程序控制。
- 如果线程不设置明确的未捕获到的异常处理程序,并且该线程的线程组(包括父线程组)未特别指定其 uncaughtException 方法,则将调用默认处理程序的 uncaughtException 方法。
- 通过设置未捕获到的默认异常处理程序,应用程序可以为那些已经接受系统提供的任何“默认”行为的线程改变未捕获到的异常处理方式(如记录到某一特定设备或文件)。
public static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
- 返回线程由于未捕获到异常而突然终止时调用的默认处理程序。
- 如果返回值为 null,则没有默认处理程序。
Thread.UncaughtExceptionHandler
public static interface Thread.UncaughtExceptionHandler
所有已知实现类:ThreadGroup
- 当 Thread 因未捕获的异常而突然终止时,调用处理程序的接口。
- 当某一线程因未捕获的异常而即将终止时,Java 虚拟机将使用
Thread.getUncaughtExceptionHandler()
查询该线程以获得其 UncaughtExceptionHandler 的线程,并调用处理程序的uncaughtException
方法,将线程和异常作为参数传递。 - 如果某一线程没有明确设置其 UncaughtExceptionHandler,则将它的 ThreadGroup 对象作为其 UncaughtExceptionHandler。
- 如果 ThreadGroup 对象对处理异常没有什么特殊要求,那么它可以将调用转发给默认的未捕获异常处理程序。
uncaughtException
void uncaughtException(Thread t, Throwable e)
- 当给定线程因给定的未捕获异常而终止时,调用该方法。
- Java 虚拟机将忽略该方法抛出的任何异常。
ThreadGroup 的 uncaughtException 方法说明
- 当此线程组中的线程因为一个未捕获的异常而停止,并且线程没有安装特定
Thread.UncaughtExceptionHandler
时,由Java Virtual Machine
调用此方法。 - 应用程序可以重写 ThreadGroup 的子类中的方法,以提供处理未捕获异常的替代办法。
源码剖析
查看 Thread 的源码可以帮忙分析当线程出现未捕获异常时的处理逻辑。
JVM 负责分发
首先看Thread.dispatchUncaughtException()
方法:
//Dispatch an uncaught exception to the handler. This method is intended to be called only by the JVM.
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
这个方法仅仅被 JVM 调用,用来将 uncaught exception
分发到 handler 去处理(主意:caught exception
必须自己捕获,这个从方法名 dispatchUncaughtException
也可以看出来)。
先找到 handler
这个 handler 是哪来的呢?
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group;
}
Returns the handler invoked when this thread abruptly terminates due to an uncaught exception.
返回此线程由于未捕获的异常而突然终止时调用的handler。
If this thread has not had an uncaught exception handler explicitly set then this thread's ThreadGroup object is returned, unless this thread has terminated, in which case null is returned.
如果此线程没有显式设置未捕获的异常 handler,则返回此线程的 ThreadGroup 对象,除非此线程已终止,在这种情况下返回 null。
这里的uncaughtExceptionHandler
只有一个地方初始化:
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;// null unless explicitly set
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
这里暴露的setUncaughtExceptionHandler
就是我们经常需要调用的,只有我们显示调用了此方法,uncaughtExceptionHandler 才有值(null unless explicitly set)。
同样的,defaultUncaughtExceptionHandler
也只有一个地方初始化:
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;// null unless explicitly set
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
//检查权限
defaultUncaughtExceptionHandler = eh;
}
再调用 uncaughtException 方法
下面一段代码是最核心的:
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) { //不是ThreadDeath
System.err.print("Exception in thread "" + t.getName() + "" "); //打印线程名字
e.printStackTrace(System.err); //打印异常
}
}
}
总结
- 如果 try catch 住了异常
- 任何
UncaughtExceptionHandler
都不会处理此异常 - 此异常不会导致当前线程直接结束,但是
try
中异常后面的代码将不再继续执行 - 不会自动打印异常堆栈,除非你在
catch
代码块中手动打印
- 任何
- 如果通过
setUncaughtExceptionHandler
显示设置了当前线程的 handler
- JVM 会直接调用此 handler 的
uncaughtException
方法 - 通常我们会重写此方法,所以
uncaught exception
分发到了我们定义的 handler 中由我们自己决定如何处理
- JVM 会直接调用此 handler 的
- 如果我们没有通过
setUncaughtExceptionHandler
显示设置当前线程的 handler
- 则首先判断 parent(也即ThreadGroup)是否为 null,如果不为 null 的话,则会调用 parent 的
uncaughtException
方法 - 如果为 null 的话,则会找 Thread 类共用的
defaultUncaughtExceptionHandler
- 如果此 handler 存在,则调用此 handler 的
uncaughtException
方法 - 如果此 handler 不存在,则打印线程名字和异常信息,然后结束当前线程
- 如果此 handler 存在,则调用此 handler 的
- 则首先判断 parent(也即ThreadGroup)是否为 null,如果不为 null 的话,则会调用 parent 的
测试案例
基础设置
Thread.setDefaultUncaughtExceptionHandler((t, e) -> System.out.println("默认Handler捕获到了异常"));
Thread.currentThread().setUncaughtExceptionHandler((t, e) -> System.out.println("捕获到了异常"));
private static void test() throws ArithmeticException {
System.out.println(1 / 0);
}
设置了 Handler,没有 try catch
System.out.println(1 / 0); //或test();
System.out.println("正常结束"); //不会执行
结论:
- 默认
Handler
和 当前线程的Handler
都没有设置时- 会导致当前线程直接结束,也即后续的代码都不会再执行(不会执行最后的打印)
- 会自动打印异常堆栈
- 默认
Handler
或 当前线程的Handler
只有一个设置时- 都可以处理当前线程的异常
- 同样会导致当前线程直接结束(不会执行最后的打印)
- 不会自动打印异常堆栈,除非你在
Handler
中手动打印
- 默认
Handler
和 当前线程的Handler
都设置时,只有当前线程的Handler
会处理当前线程的异常
有 try catch 代码
try {
System.out.println(1 / 0); //或test();
System.out.println("try中不再继续执行");
} catch (ArithmeticException e) {
System.out.println(捕获到了异常");
}
System.out.println("正常结束"); //会执行
结论
- 如果 try catch 住了异常,则任何
UncaughtExceptionHandler
都不会处理此异常 - 不会导致当前线程直接结束(会执行最后的打印),但是
try
中代码将不再继续执行 - 不会自动打印异常堆栈,除非你在
catch
代码块中手动打印
JAVA 测试案例
Test
public class Test {
public static void main(String[] args) {
setDefaultUncaughtExceptionHandler();
test();
}
private static void test() {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程异常前");
System.out.println(1 / 0);
}
}).start();
System.out.println("当前线程异常前");
System.out.println(1 / 0);
System.out.println("异常后的代码不能执行了");
}
private static void setDefaultUncaughtExceptionHandler() {
UncaughtExceptionHandler currentHandler = new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("【当前线程的Handler处理异常信息】" + t.toString() + "
" + e.getMessage());
}
};
UncaughtExceptionHandler defaultHandler = new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
printWriter.write("start------------
");
e.printStackTrace(printWriter);
printWriter.write("------------end");
printWriter.close();
System.out.println("【默认的Handler处理异常信息】" + writer.getBuffer().toString());
}
};
Thread.currentThread().setUncaughtExceptionHandler(currentHandler);
Thread.setDefaultUncaughtExceptionHandler(defaultHandler);
}
}
运行结果
子线程异常前
当前线程异常前
【当前线程的Handler处理异常信息】Thread[main,5,main]
/ by zero
【默认的Handler处理异常信息】start------------
java.lang.ArithmeticException: / by zero
at Test$1.run(Test.java:16)
at java.lang.Thread.run(Thread.java:745)
------------end
Android 中的一个实用案例
异常处理类
/**
* Desc:采集崩溃日志
*
* @author <a href="http://www.cnblogs.com/baiqiantao">白乾涛</a><p>
* @tag 崩溃日志<p>
* @date 2018/5/2 14:12 <p>
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String LOG_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/crashLog/";
private Application application;
private static CrashHandler instance = new CrashHandler();
private CrashHandler() {//构造方法私有
}
public static CrashHandler getInstance() {
return instance;
}
public void init(Application application) {
this.application = application;
Thread.setDefaultUncaughtExceptionHandler(instance);//设置该CrashHandler为系统默认的
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
saveInfoToFile(collectCrashInfo(ex));//保存错误信息
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(application, "程序开小差了,将会在2秒后退出", Toast.LENGTH_SHORT).show();//使用Toast来显示异常信息
Looper.loop();
}
}.start();
SystemClock.sleep(2000);//延迟2秒杀进程
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private String collectCrashInfo(Throwable ex) {
if (ex == null) return "";
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable throwable = ex.getCause();
while (throwable != null) {
throwable.printStackTrace(printWriter);
throwable = throwable.getCause();//逐级获取错误信息
}
String crashInfo = writer.toString();
Log.i("bqt", "【错误信息】" + crashInfo);
printWriter.close();
return crashInfo;
}
private void saveInfoToFile(String crashInfo) {
try {
File dir = new File(LOG_PATH);
if (!dir.exists()) {
dir.mkdirs();
}
String date = new SimpleDateFormat("yyyy.MM.dd_HH_mm_ss", Locale.getDefault()).format(new Date());
String fileName = LOG_PATH + "crash_" + date + ".txt";
FileWriter writer = new FileWriter(fileName);//如果保存失败,很可能是没有写SD卡权限
writer.write(crashInfo);
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
获取的异常信息
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
at com.bqt.test.MainActivity.onListItemClick(MainActivity.java:34)
at android.app.ListActivity$2.onItemClick(ListActivity.java:319)
at android.widget.AdapterView.performItemClick(AdapterView.java:339)
at android.widget.AbsListView.performItemClick(AbsListView.java:1705)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:4171)
at android.widget.AbsListView$13.run(AbsListView.java:6735)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6682)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1534)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1424)
堆栈跟踪 StackTraceElement
StackTraceElement 文档
此类在 java.lang
包下
public final class StackTraceElement extends Object implements Serializable
堆栈跟踪元素,它由 Throwable.getStackTrace()
返回。每个元素表示单独的一个【堆栈帧】。所有的堆栈帧(堆栈顶部的那个堆栈帧除外)都表示一个【方法调用】。堆栈顶部的帧表示【生成堆栈跟踪的执行点】。通常,这是创建对应于堆栈跟踪的 throwable 的点。
构造方法
public StackTraceElement(String declaringClass, String methodName, String fileName, int lineNumber)
创建表示指定【执行点】的【堆栈跟踪元素】。
参数:
- declaringClass - 类的完全限定名,该类包含由堆栈跟踪元素所表示的执行点
- methodName - 方法名,该方法包含由堆栈跟踪元素所表示的执行点
- fileName - 文件名,该文件包含由堆栈跟踪元素所表示的执行点;如果该信息不可用,则该参数为 null
- lineNumber - 源代码行的行号,该代码行包含由堆栈跟踪元素所表示的执行点;如果此信息不可用,则该参数为负数。值 -2 表示包含执行点的方法是一个本机方法
普通方法
- String getClassName() 返回类的完全限定名,该类包含由该堆栈跟踪元素所表示的执行点。
- String getFileName() 返回源文件名,该文件包含由该堆栈跟踪元素所表示的执行点。
- int getLineNumber() 返回源行的行号,该行包含由该堆栈该跟踪元素所表示的执行点。
- String getMethodName() 返回方法名,此方法包含由该堆栈跟踪元素所表示的执行点。
- boolean isNativeMethod() 如果包含由该堆栈跟踪元素所表示的执行点的方法是一个本机方法,则返回 true。
重写的Object的方法
- boolean equals(Object obj) 如果指定的对象是另一个 StackTraceElement 实例,并且该对象表示的【执行点】与该实例的相同,则返回 ture。
- int hashCode() 返回此堆栈跟踪元素的哈希码值。
- String toString() 返回表示该堆栈跟踪元素的字符串。
该字符串的格式取决于实现,但是可将MyClass.mash(MyClass.java:9)
视为典型的格式,其中:
MyClass
是类的完全限定名,该类包含由该堆栈跟踪元素所表示的执行点;mash
是包含执行点的方法的名字;MyClass.java
是包含执行点的源文件;9
是包含执行点的源行的行号。
其他格式
MyClass.mash(MyClass.java)
- 同上,但是行号不可用。MyClass.mash(Unknown Source)
- 同上,但是文件名和行号都不可用。MyClass.mash(Native Method)
- 同上,但是文件名和行号都不可用,并且已知包含执行点的方法是本机方法。
示例1:获取 StackTraceElement[]
Test
public class Test {
public static void main(String[] args) {
printCallStatck();
}
public static void printCallStatck() {
StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
//StackTraceElement[] stackElements = new Throwable().getStackTrace();
if (stackElements != null) {
for (StackTraceElement stackTraceElement : stackElements) {
System.out.println("ClassName:" + stackTraceElement.getClassName());
System.out.println("FileName:" + stackTraceElement.getFileName());
System.out.println("LineNumber:" + stackTraceElement.getLineNumber());
System.out.println("MethodName:" + stackTraceElement.getMethodName());
System.out.println("isNativeMethod:" + stackTraceElement.isNativeMethod());
System.out.println("信息:" + stackTraceElement.toString());
System.out.println("-----------------------------------");
}
}
}
}
结果
ClassName:java.lang.Thread
FileName:null
LineNumber:-1
MethodName:getStackTrace
isNativeMethod:false
信息:java.lang.Thread.getStackTrace(Unknown Source)
-----------------------------------
ClassName:Test
FileName:Test.java
LineNumber:7
MethodName:printCallStatck
isNativeMethod:false
信息:Test.printCallStatck(Test.java:7)
-----------------------------------
ClassName:Test
FileName:Test.java
LineNumber:3
MethodName:main
isNativeMethod:false
信息:Test.main(Test.java:3)
-----------------------------------
注意
1、数组中元素的个数是不确定个的,这完全取决于打印堆栈信息时经过了几步的调用。
2、并非最后一个元素的就一定是我们最想要的信息,因为在框架中,后续可能还会有很多层的调用,比如在Android中打印日志的一个案例:
dalvik.system.VMStack:getThreadStackTrace:-2
java.lang.Thread:getStackTrace:1566
com.yibasan.lizhifm.ad.Lg:log:58
com.yibasan.lizhifm.ad.Lg:i:34//这里调用了Log.i
com.yibasan.lizhifm.ad.SplashAdManager:canShowAd:66//我们是在这里调用了封装好的Lg类中的Lg.i方法,所以这里的元素是我们最想要的
com.yibasan.lizhifm.activities.BaseActivity:onResume:518
com.yibasan.lizhifm.activities.fm.NavBarActivity:onResume:375
android.app.Instrumentation:callActivityOnResume:1276
android.app.Activity:performResume:6937
android.app.ActivityThread:performResumeActivity:3468
android.app.ActivityThread:handleResumeActivity:3531
android.app.ActivityThread$H:handleMessage:1569
android.os.Handler:dispatchMessage:102
android.os.Looper:loop:154
android.app.ActivityThread:main:6209
java.lang.reflect.Method:invoke:-2
com.android.internal.os.ZygoteInit$MethodAndArgsCaller:run:900
com.android.internal.os.ZygoteInit:main:790
示例2:通过 printStackTrace 打印
public class Test {
public static void main(String[] args) {
printCallStatck();
}
public static void printCallStatck() {
new Throwable("创建一个Throwable对象").printStackTrace();
}
}
结果
java.lang.Throwable: 创建一个Throwable对象
at Test.printCallStatck(Test.java:7)
at Test.main(Test.java:3)
2018-05-31