@TryLoveCatch
2022-05-18T03:45:32.000000Z
字数 13953
阅读 2303
Android知识体系
Thread ThreadGroup的关系 异常捕获
java子线程发生异常,默认不会捕获,如何捕获?
Android子线程异常,却可以捕获?
setDefaultUncaughtExceptionHandler
如果三方也设置了setDefaultUncaughtExceptionHandler,自己的app怎么处理,才能不能覆盖
无论是Java还是Android项目,往往都会用到多线程。不管是主线程还是子线程,在运行过程中,都有可能出现未捕获异常。未捕获异常中含有详细的异常信息堆栈,可以很方便的去帮助我们排查问题。
默认情况下,异常信息堆栈都会在输出设备显示,同时,Java & Android为我们提供了未捕获异常的处理接口,使得我们可以去自定义异常的处理,甚至可以改变在异常处理流程上的具体走向,如常见的将异常信息写到本地日志文件,甚至上报服务端等。
在未捕获异常的处理机制上,总体上,Android基本沿用了Java的整套流程,同时,针对Android自身的特点,进行了一些特别的处理,使得在表现上与Java默认的流程会有一些差异。
/*** 作用:* 1.收集错误信息* 2.保存错误信息*/public class CrashHandler implements Thread.UncaughtExceptionHandler {private static CrashHandler sInstance = null;private Thread.UncaughtExceptionHandler mDefaultHandler;private Context mContext;// 保存手机信息和异常信息private Map<String, String> mMessage = new HashMap<>();public static CrashHandler getInstance() {if (sInstance == null) {synchronized (CrashHandler.class) {if (sInstance == null) {synchronized (CrashHandler.class) {sInstance = new CrashHandler();}}}}return sInstance;}private CrashHandler() {}/*** 初始化默认异常捕获** @param context context*/public void init(Context context) {mContext = context;// 获取默认异常处理器mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();// 将此类设为默认异常处理器Thread.setDefaultUncaughtExceptionHandler(this);}@Overridepublic void uncaughtException(Thread t, Throwable e) {if (!handleException(e)) {// 未经过人为处理,则调用系统默认处理异常,弹出系统强制关闭的对话框if (mDefaultHandler != null) {mDefaultHandler.uncaughtException(t, e);}} else {// 已经人为处理,系统自己退出try {Thread.sleep(1000);} catch (InterruptedException e1) {e1.printStackTrace();}Process.killProcess(Process.myPid());System.exit(1);}}/*** 是否人为捕获异常** @param e Throwable* @return true:已处理 false:未处理*/private boolean handleException(Throwable e) {if (e == null) {// 异常是否为空return false;}new Thread() {// 在主线程中弹出提示@Overridepublic void run() {Looper.prepare();Toast.makeText(mContext, "捕获到异常", Toast.LENGTH_SHORT).show();Looper.loop();}}.start();collectErrorMessages();// 保存信息等操作return false;}/*** 1.收集错误信息*/private void collectErrorMessages() {PackageManager pm = mContext.getPackageManager();try {PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);if (pi != null) {String versionName = TextUtils.isEmpty(pi.versionName) ? "null" : pi.versionName;String versionCode = "" + pi.versionCode;mMessage.put("versionName", versionName);mMessage.put("versionCode", versionCode);}// 通过反射拿到错误信息Field[] fields = Build.class.getFields();if (fields != null && fields.length > 0) {for (Field field : fields) {field.setAccessible(true);try {mMessage.put(field.getName(), field.get(null).toString());} catch (IllegalAccessException e) {e.printStackTrace();}}}} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();}}}
在自定义Application里面执行:
CrashHandler.getInstance().init(this);
我们先可以思考几个问题:
package com.corn.javalib;public class MyClass {public static void main(String[] args) {System.out.println("thread name:" + Thread.currentThread().getName() + " begin...");Thread thread = new Thread(new MyRunnable());thread.start();try {Thread.currentThread().sleep(1000);} catch (Exception e) {e.printStackTrace();}System.out.println("thread name:" + Thread.currentThread().getName() + " end...");}static class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("thread name:" + Thread.currentThread().getName() + " start run");errorMethod();System.out.println("thread name:" + Thread.currentThread().getName() + " end run");}}public static int errorMethod() {String name = null;return name.length();}}
执行Java程序,最后输出结果为:
thread name:main begin...thread name:Thread-0 start runException in thread "Thread-0" java.lang.NullPointerExceptionat com.corn.javalib.MyClass.errorMethod(MyClass.java:35)at com.corn.javalib.MyClass$MyRunnable.run(MyClass.java:26)at java.lang.Thread.run(Thread.java:748)thread name:main end...Process finished with exit code 0
我们发现,主线程中新起的子线程在运行时,出现了未捕获异常,但是,main主线程还是可以继续执行下去的,对整个进程而言,最终是Process finished with exit code 0,说明也没有异常终止。
因此,第一个问题的结果是:
Java子线程中出现了未捕获的异常,默认情况下不会导致主进程异常终止。
同样的,新建Android工程后,模拟对应的场景,例如点击按钮,启动子线程,发现App直接闪退,AS Logcat中对应有如下日志输出:
2019-11-21 19:10:42.678 26259-26449/com.corn.crash I/System.out: thread name:Thread-2 start run2019-11-21 19:10:42.679 26259-26449/com.corn.crash E/AndroidRuntime: FATAL EXCEPTION: Thread-2Process: com.corn.crash, PID: 26259java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object referenceat com.corn.crash.MainActivity.errorMethod(MainActivity.java:76)at com.corn.crash.MainActivity$MyRunnable.run(MainActivity.java:67)at java.lang.Thread.run(Thread.java:764)2019-11-21 19:10:42.703 26259-26449/com.corn.crash I/Process: Sending signal. PID: 26259 SIG: 9
从日志信息上看,SIG: 9,对应SIGKILL,意味着App进程被kill掉(参考),日志信息堆栈中给出了具体的异常位置,于是,我们得出如下结论:
默认情况下,Android子线程中出现了未捕获的异常,在是会导致App闪退的,且有异常信息堆栈输出。
我们发现,基于Java基础上的Android,默认情况下,对于子线程中的未捕获异常,在进程是否异常退出方面,却有着相反的结果:
我们先来看看Java的异常处理流程,具体参考Thread类:
注:图中,红框里面的是静态方法
public final void dispatchUncaughtException(Throwable e) {Thread.UncaughtExceptionHandler initialUeh =Thread.getUncaughtExceptionPreHandler();if (initialUeh != null) {try {initialUeh.uncaughtException(this, e);} catch (RuntimeException | Error ignored) {// Throwables thrown by the initial handler are ignored}}getUncaughtExceptionHandler().uncaughtException(this, e);}
public UncaughtExceptionHandler getUncaughtExceptionHandler() {return uncaughtExceptionHandler != null ?uncaughtExceptionHandler : group;}
public Thread() {init(null, null, "Thread-" + nextThreadNum(), 0);}private void init(ThreadGroup g, Runnable target, String name, long stackSize) {Thread parent = currentThread();if (g == null) {g = parent.getThreadGroup();}}
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)) {System.err.print("Exception in thread \""+ t.getName() + "\" ");e.printStackTrace(System.err);}}}
如果父线程组不为空,则递归调用父线程组的uncaughtException,直到为null,就调用Thread.getDefaultUncaughtExceptionHandler()来处理。除非重写了ThreadGroup#uncaughtException,才不会执行Thread.getDefaultUncaughtExceptionHandler()
当Android项目中出现未捕获异常时,Logcat中默认会自动有异常堆栈信息输出,且信息输出的前缀为:
E/AndroidRuntime: FATAL EXCEPTION:。我们很容易猜想到,这应该是系统层直接输出的,搜索framework源码,很快可以找到具体输出日志的位置在RuntimeInit.java中。
@UnsupportedAppUsagepublic static final void main(String[] argv) {enableDdms();if (argv.length == 2 && argv[1].equals("application")) {if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application");redirectLogStreams();} else {if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting tool");}commonInit();/** Now that we're running in interpreted code, call back into native code* to run the system.*/nativeFinishInit();if (DEBUG) Slog.d(TAG, "Leaving RuntimeInit!");}
private static final void commonInit() {if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");/** set handlers; these apply to all threads in the VM. Apps can replace* the default handler, but not the pre handler.*/LoggingHandler loggingHandler = new LoggingHandler();RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));//...//...}
/*** Logs a message when a thread encounters an uncaught exception. By* default, {@link KillApplicationHandler} will terminate this process later,* but apps can override that behavior.*/private static class LoggingHandler implements Thread.UncaughtExceptionHandler {public volatile boolean mTriggered = false;@Overridepublic void uncaughtException(Thread t, Throwable e) {mTriggered = true;// Don't re-enter if KillApplicationHandler has already runif (mCrashing) return;// mApplicationObject is null for non-zygote java programs (e.g. "am")// There are also apps running with the system UID. We don't want the// first clause in either of these two cases, only for system_server.if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);} else {StringBuilder message = new StringBuilder();// The "FATAL EXCEPTION" string is still used on Android even though// apps can set a custom UncaughtExceptionHandler that renders uncaught// exceptions non-fatal.message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");final String processName = ActivityThread.currentProcessName();if (processName != null) {message.append("Process: ").append(processName).append(", ");}message.append("PID: ").append(Process.myPid());Clog_e(TAG, message.toString(), e);}}}
/*** Handle application death from an uncaught exception. The framework* catches these for the main threads, so this should only matter for* threads created by applications. Before this method runs, the given* instance of {@link LoggingHandler} should already have logged details* (and if not it is run first).*/private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {private final LoggingHandler mLoggingHandler;/*** Create a new KillApplicationHandler that follows the given LoggingHandler.* If {@link #uncaughtException(Thread, Throwable) uncaughtException} is called* on the created instance without {@code loggingHandler} having been triggered,* {@link LoggingHandler#uncaughtException(Thread, Throwable)* loggingHandler.uncaughtException} will be called first.** @param loggingHandler the {@link LoggingHandler} expected to have run before* this instance's {@link #uncaughtException(Thread, Throwable) uncaughtException}* is being called.*/public KillApplicationHandler(LoggingHandler loggingHandler) {this.mLoggingHandler = Objects.requireNonNull(loggingHandler);}@Overridepublic void uncaughtException(Thread t, Throwable e) {try {ensureLogging(t, e);// Don't re-enter -- avoid infinite loops if crash-reporting crashes.if (mCrashing) return;mCrashing = true;// Try to end profiling. If a profiler is running at this point, and we kill the// process (below), the in-memory buffer will be lost. So try to stop, which will// flush the buffer. (This makes method trace profiling useful to debug crashes.)if (ActivityThread.currentActivityThread() != null) {ActivityThread.currentActivityThread().stopProfiling();}// Bring up crash dialog, wait for it to be dismissedActivityManager.getService().handleApplicationCrash(mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));} catch (Throwable t2) {if (t2 instanceof DeadObjectException) {// System process is dead; ignore} else {try {Clog_e(TAG, "Error reporting crash", t2);} catch (Throwable t3) {// Even Clog_e() fails! Oh well.}}} finally {// Try everything to make sure this process goes away.Process.killProcess(Process.myPid());System.exit(10);}}/*** Ensures that the logging handler has been triggered.** See b/73380984. This reinstates the pre-O behavior of** {@code thread.getUncaughtExceptionHandler().uncaughtException(thread, e);}** logging the exception (in addition to killing the app). This behavior* was never documented / guaranteed but helps in diagnostics of apps* using the pattern.** If this KillApplicationHandler is invoked the "regular" way (by* {@link Thread#dispatchUncaughtException(Throwable)* Thread.dispatchUncaughtException} in case of an uncaught exception)* then the pre-handler (expected to be {@link #mLoggingHandler}) will already* have run. Otherwise, we manually invoke it here.*/private void ensureLogging(Thread t, Throwable e) {if (!mLoggingHandler.mTriggered) {try {mLoggingHandler.uncaughtException(t, e);} catch (Throwable loggingThrowable) {// Ignored.}}}}
主要是这几句:
LoggingHandler loggingHandler = new LoggingHandler();RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
所以Android默认设置了uncaughtExceptionPreHandler和defaultUncaughtExceptionHandler
| 变量 | 描述 | 静态? | 隐藏? | Android默认值 | 备注 |
|---|---|---|---|---|---|
| uncaughtExceptionPreHandler | 进程唯一的异常预处理器 | 是 | 是 | LoggingHandler,用来打印log,也就是FATAL EXCEPTION | 线程共享 |
| uncaughtExceptionHandler | 线程单独的异常处理器 | 否 | 否 | 无 | 线程独有 |
| defaultUncaughtExceptionHandler,弹出crash dialog并且杀死进程 | 进程唯一的默认预处理器 | 是 | 否 | KillApplicationHandler | 线程共享 |
默认情况下,未捕获异常发生时,Logcat中的异常堆栈信息,是从framework层,
具体是RuntimeInit.java类中的loggingHandler异常处理处理对象中的uncaughtException输出。
例如项目中接入了腾讯的bugly,同时又接入了友盟或firebase,且项目自身,往往还自定义了异常处理器。这在实际项目开发中是非常常见的。
我们可以非常肯定的说:当有未捕获异常出现时,多个质量监控的后台,都能有效收集到对应的错误信息。这也是实际上都知道的“常识”
但是,为什么彼此之间没有互相冲突,也没有相互影响呢?
原因在于大家都是遵循同样的一套原则去处理未捕获的异常,而未实际去阻断或不可逆的直接改变未捕获异常的流程:
如此,表面上看,是static静态变量(线程默认的异常处理器)每次被重新覆盖,实际上却达到了彼此间的自定义的异常处理逻辑都能实现,互不影响。
可以参考文章开头的那个使用,就是这种处理方式。
这个根据上面的分析,答案显而易见,不赘述了
https://juejin.cn/post/6931939584548798471
总的来说就是通过信号量来实现的。
信号量捕获机制是建立在Linux系统底层的信号机制之上的方法,系统层会在发生崩溃的时候发送一些特定信号,通过捕获并处理这些特定信号,我们就能够避免JNI crash的发生,从而相对优雅的结束程序的执行。

这两种方式都是可以跳过defaultUncaughtExceptionHandler。
在Android环境下,可以通过重写Thread#setUncaughtExceptionHandler(),来避免该线程崩溃,从来引发App崩溃
拦截主进程崩溃其实也有一定的弊端,因为给用户的感觉是点击没有反应,因为崩溃已经被拦截了。如果是Activity.create崩溃,会出现黑屏问题,所以如果Activity.create崩溃,必须杀死进程,让APP重启,避免出现改问题。
public class MyApplication extends Application {@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);new Handler(getMainLooper()).post(new Runnable() {@Overridepublic void run() {while (true) {try {Looper.loop();} catch (Throwable e) {e.printStackTrace();// TODO 需要手动上报错误到异常管理平台,比如bugly,及时追踪问题所在。if (e.getMessage() != null && e.getMessage().startsWith("Unable to start activity")) {// 如果打开Activity崩溃,就杀死进程,让APP重启。android.os.Process.killProcess(android.os.Process.myPid());break;}}}}});}}