@TryLoveCatch
2022-05-18T11:45:32.000000Z
字数 13953
阅读 1552
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);
}
@Override
public 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() {// 在主线程中弹出提示
@Override
public 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 {
@Override
public 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 run
Exception in thread "Thread-0" java.lang.NullPointerException
at 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 run
2019-11-21 19:10:42.679 26259-26449/com.corn.crash E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.corn.crash, PID: 26259
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
at 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中。
@UnsupportedAppUsage
public 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;
@Override
public void uncaughtException(Thread t, Throwable e) {
mTriggered = true;
// Don't re-enter if KillApplicationHandler has already run
if (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);
}
@Override
public 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 dismissed
ActivityManager.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 {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
new Handler(getMainLooper()).post(new Runnable() {
@Override
public 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;
}
}
}
}
});
}
}