End

异常 Exception 异常捕获 堆栈跟踪

本文地址


目录

异常 Exception 异常捕获 堆栈跟踪

异常捕获

常用 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 中由我们自己决定如何处理
  • 如果我们没有通过setUncaughtExceptionHandler显示设置当前线程的 handler

    • 则首先判断 parent(也即ThreadGroup)是否为 null,如果不为 null 的话,则会调用 parent 的uncaughtException方法
    • 如果为 null 的话,则会找 Thread 类共用的defaultUncaughtExceptionHandler
      • 如果此 handler 存在,则调用此 handler 的uncaughtException方法
      • 如果此 handler 不存在,则打印线程名字和异常信息,然后结束当前线程

测试案例

基础设置

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() + "\n" + 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------------\n");
                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

posted @ 2018-05-31 23:40  白乾涛  阅读(8720)  评论(0编辑  收藏  举报