Android未捕获异常监控原理
创始人
2024-04-10 01:47:03

背景

  • 本文仅探讨java层的未捕获异常的监控
  • 为什么我们自己的异常捕获总是比 Bugly 收到的信息少?

Android未捕获异常的监控与收集

Java层未捕获异常监控的基本实现

先看看Java层未捕获异常监控的运行过程:

public class MyUncaughtExceptionHandler implements UncaughtExceptionHandler {private static UncaughtExceptionHandler oriHandler;private static volatile boolean disable = false;public static void disable() {disable = true;}public static void register() {if (!disable) {// 1.保存原有的UncaughtExceptionHandler实例oriHandler = Thread.getDefaultUncaughtExceptionHandler();// 2.设置自定义的UncaughtExceptionHandler实例,替代原有的Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());}}private MyUncaughtExceptionHandler() {}public void uncaughtException(Thread thread, Throwable ex) {// 3.收到未捕获异常时,进行相应的业务处理handleCrash(ex);// 4.将异常信息继续丢回给原有的UncaughtExceptionHandlerif (oriHandler != null) {oriHandler.uncaughtException(thread, ex);}}
}

提问:有没有可能某些第三方SDK甚至应用自己私吞异常,不向下传递?
对于这个猜测的证伪,需要从源码层面分析,下面来看看。

Framework 对未捕获异常的处理流程分析

应用启动流程中会执行到 RumtimeInit.commonInit() 中:

// RuntimeInit.java
protected 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 与 KillApplicationHandler 都是 UncaughtExceptionHandler 的实现类// UncaughtExceptionPreHandler 无法被应用替换,只有系统能用LoggingHandler loggingHandler = new LoggingHandler();RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);// defaultUncaughtExceptionHandler 是可以被应用替换掉的Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));...
}

可见,系统在应用启动时,同时注册了 LoggingHandler 与 KillApplicationHandler 两个 UncaughtExceptionHandler 的实现类,用于监听未捕获异常。为什么这么做呢,我们分别分析两个实现类的流程:

先分析 RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler) :

// RuntimeHooks.java
public static void setUncaughtExceptionPreHandler(Thread.UncaughtExceptionHandler uncaughtExceptionHandler) {// 直接调用Thread.setUncaughtExceptionPreHandler()Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler);
}
// Thread.java
// Thread 同时提供了UncaughtExceptionPreHandler的set/get方法
public static void setUncaughtExceptionPreHandler(UncaughtExceptionHandler eh) {uncaughtExceptionPreHandler = eh;
}// getUncaughtExceptionPreHandler() 方法会被 Thread.dispatchUncaughtException(Throwable e) 调用
public static UncaughtExceptionHandler getUncaughtExceptionPreHandler() {return uncaughtExceptionPreHandler;
}// 分发异常
public final void dispatchUncaughtException(Throwable e) {// BEGIN Android-added: uncaughtExceptionPreHandler for use by platform.// 分发给 UncaughtExceptionPreHandlerThread.UncaughtExceptionHandler initialUeh =Thread.getUncaughtExceptionPreHandler();if (initialUeh != null) {try {initialUeh.uncaughtException(this, e);} catch (RuntimeException | Error ignored) {// Throwables thrown by the initial handler are ignored}}// END Android-added: uncaughtExceptionPreHandler for use by platform.// 分发给 UncaughtExceptionHandler(就是应用可以自行注册的UEH)getUncaughtExceptionHandler().uncaughtException(this, e);
}

可见,系统会将未捕获异常同时分发给 uncaughtExceptionPreHandler 和 UncaughtExceptionHandler。所以,接下来就要弄明白这两个 UEH 各自做了什么,先看 LoggingHandler :

// RuntimeInit.java
// LoggingHandler 是 RuntimeInit 的内部类
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 {// 大部分情况都会走到这里logUncaught(t.getName(), ActivityThread.currentProcessName(), Process.myPid(), e);}}
}public static void logUncaught(String threadName, String processName, int pid, Throwable e) {// 生成一段msgStringBuilder 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(threadName).append("\n");if (processName != null) {message.append("Process: ").append(processName).append(", ");}message.append("PID: ").append(pid);// 打印Clog_e(TAG, message.toString(), e);
}private static int Clog_e(String tag, String msg, Throwable tr) {return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
}

可见,LoggingHandler 在收到异常回调时,仅仅做了一些系统打印操作。那么,KillApplicationHandler 呢?

// RuntimeInit.java
// KillApplicationHandler 是 RuntimeInit 的内部类
/*** 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 {// 持有 pre-handler(LoggingHandler)的引用,作用是确保 LoggingHandler.uncaughtException() 被触发private final LoggingHandler mLoggingHandler;public KillApplicationHandler(LoggingHandler loggingHandler) {this.mLoggingHandler = Objects.requireNonNull(loggingHandler);}@Overridepublic void uncaughtException(Thread t, Throwable e) {try {// 确保 LoggingHandler.uncaughtException() 被触发ensureLogging(t, e);// Don't re-enter -- avoid infinite loops if crash-reporting crashes.if (mCrashing) return;mCrashing = true;/** 如果正在使用 profiler 跟踪代码,就应该停止 profiling,否则杀死应用进程时会导致 * profiler 的内存数据丢失,而正确的 stopProfiling,可以保证用户能通过 profiler 跟踪crash*/// 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) {// 如果应用的UEH拦截了异常,不传回给系统,主要有影响的其实就是这里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.}}}
}

最后,让我们再多看一眼 ActivityThread.currentActivityThread().stopProfiling() ,确认一下它到底是不是如推测的那样,是控制 Profiler 的:

// ActivityThread.java
/*** Public entrypoint to stop profiling. This is required to end profiling when the app crashes,* so that profiler data won't be lost.** @hide*/
public void stopProfiling() {if (mProfiler != null) {mProfiler.stopProfiling();}
}// Profiler 是 ActivityThread 的内部类
static final class Profiler {...public void stopProfiling() {if (profiling) {profiling = false;// traceview 等工具做代码跟踪时,都会用到的方法Debug.stopMethodTracing();if (profileFd != null) {try {profileFd.close();} catch (IOException e) {}}profileFd = null;profileFile = null;}}
}

至此,Framework 处理未捕获异常的完整流程就分析完了。

最后做个总结
  • 系统会同时注册两个 UncaughtExceptionHandler 实例:LoggingHandler 和 KillApplicationHandler
LoggingHandlerKillApplicationHandler
注册为pre-handlerhandler
可替换仅可被系统使用,不能被替换可被应用替换
作用在 Logcat 中输出崩溃日志1.确保 LoggingHandler 被触发
2.停止 Profiler(如果正在 profiling 的话)
3.弹出崩溃提示框
4.结束进程

完整的未捕获异常处理流程

Android未捕获异常处理流程.png

回到最初的“猜测”

基于以上分析,文初的“猜测”基本上不存在可能性,原因如下:

  • 异常的传递是必须的,最终要传回给系统进行处理,如果私吞,应用会停留在前台,但无法响应任何操作,这不符合大部分产品的设计理念

当然,你可以私吞异常,并且自行杀死进程,以避免应用停留前台,如下操作:

	public void uncaughtException(Thread thread, Throwable ex) {handleCrash(ex);System.exit(10);// 不往下传递异常信息
//		if (oriHandler != null) {
//			oriHandler.uncaughtException(thread, ex);
//		}}

但作为sdk,私吞异常还会导致应用自己的UEH也捕获不到异常,会被应用投诉,得不偿失!而作为应用,如此操作会导致无法使用第三方异常监控工具,也会导致 profiler 失效,影响自己内部调试。

所以说,私吞异常是一个损人不利己的行为,甚至是害人害己。

更可靠的推测

崩溃处理流程中的一切异步操作,均有失败的风险

新的优化方向:异步 → 同步

  • 上传失败:数据安全,下次启动时会上传
  • 缓存DB失败:数据丢失,本次/下次均不会上传

因此,至少要将“缓存DB”改为同步操作,才能保证缓存成功

问题:uncaughtException()是在主线程触发的,而我们禁止主线程I/O?

Bugly 源码分析

  • crashreport:4.1.9

反编译bugly,验证其异常系统的核心实现:

// com.tencent.bugly.proguard.av.java
/*** 注册监控*/
public final synchronized void registerUeh() {if (this.j >= 10) {BuglyLog.a("java crash handler over %d, no need set.", new Object[]{Integer.valueOf(10)});return;}this.enable = true;Thread.UncaughtExceptionHandler uncaughtExceptionHandler;if ((uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()) != null) {String str1 = getClass().getName();String str2 = uncaughtExceptionHandler.getClass().getName();// 注册过的Bugly监控不再注册,避免重复注册if (str1.equals(str2)) {return;}// 当前的UEH是系统默认的,缓存起来(系统默认的在高版本中是RuntimeInit$KillApplicationHandler,bugly未做适配)if ("com.android.internal.os.RuntimeInit$UncaughtHandler".equals(uncaughtExceptionHandler.getClass().getName())) {BuglyLog.a("backup system java handler: %s", new Object[]{uncaughtExceptionHandler.toString()});this.sysDefUeh = uncaughtExceptionHandler;this.custUeh = uncaughtExceptionHandler;} else {// 当前的UEH是应用自定义的BuglyLog.a("backup java handler: %s", new Object[]{uncaughtExceptionHandler.toString()});this.custUeh = uncaughtExceptionHandler;}}// 设置Bugly的UEH实例,替代原有的Thread.setDefaultUncaughtExceptionHandler(this);this.j++;BuglyLog.a("registered java monitor: %s", new Object[]{toString()});
}
// com.tencent.bugly.proguard.av.java
public final void uncaughtException(Thread paramThread, Throwable paramThrowable) {synchronized (i) {handleException(paramThread, paramThrowable, true, null, null, this.d.Q);return;}
}/*** 异常处理** @param paramThread* @param paramThrowable* @param uncaughtException* @param paramString* @param paramArrayOfbyte* @param paramBoolean2*/
public final void handleException(Thread paramThread, Throwable paramThrowable, boolean uncaughtException, String paramString, byte[] paramArrayOfbyte, boolean paramBoolean2) {// uncaughtException = trueif (uncaughtException) {BuglyLog.e("Java Crash Happen cause by %s(%d)", new Object[]{paramThread.getName(), Long.valueOf(paramThread.getId())});// 已处理过if (isHandled(paramThread)) {BuglyLog.a("this class has handled this exception", new Object[0]);// 有系统默认的UEH,交给系统处理if (this.sysDefUeh != null) {BuglyLog.a("call system handler", new Object[0]);this.sysDefUeh.uncaughtException(paramThread, paramThrowable);} else {// 自行结束进程exit();}}} else {BuglyLog.e("Java Catch Happen", new Object[0]);}// 核心处理try {// 监控被禁用,结束if (!this.enable) {BuglyLog.c("Java crash handler is disable. Just return.", new Object[0]);return;}// 没有设置StrategyBeanif (!this.c.b()) {BuglyLog.d("no remote but still store!", new Object[0]);}// crash report被禁用,且设置了StrategyBean,则本地打印crash日志,结束if (!this.c.getStrategy().enableCrashReport && this.c.b()) {BuglyLog.e("crash report was closed by remote , will not upload to Bugly , print local for helpful!", new Object[0]);as.printLocal(uncaughtException ? "JAVA_CRASH" : "JAVA_CATCH", ap.a(), this.d.d, paramThread.getName(), ap.a(paramThrowable), null);return;}CrashDetailBean crashDetailBean;// 打包crash数据,生成CrashDetailBean,如果失败就退出if ((crashDetailBean = makeCrashDetailBean(paramThread, paramThrowable, uncaughtException, paramString, paramArrayOfbyte, paramBoolean2)) == null) {BuglyLog.e("pkg crash datas fail!", new Object[0]);return;}as.printLocal(uncaughtException ? "JAVA_CRASH" : "JAVA_CATCH", ap.a(), this.d.d, paramThread.getName(), ap.a(paramThrowable), crashDetailBean);// 保存到本地数据库,并返回操作结果(推断如果失败,不再执行上传)if (!this.b.saveDb(crashDetailBean, uncaughtException)) {// 根据BuglyStrategy的设置,决定是否要立即上传this.b.uploadOnNecessary(crashDetailBean, uncaughtException);}if (uncaughtException) {// 内部只是一个日志打印的逻辑this.b.a(crashDetailBean);}} catch (Throwable throwable) {if (!BuglyLog.a((Throwable) (paramString = null))) {paramString.printStackTrace();}} finally {// uncaughtException = trueif (uncaughtException) {// 有应用自定义的UEH,且 它没有在处理当前异常,就传给它处理if (this.custUeh != null && targetUehNotHandling(this.custUeh)) {BuglyLog.e("sys default last handle start!", new Object[0]);this.custUeh.uncaughtException(paramThread, paramThrowable);BuglyLog.e("sys default last handle end!", new Object[0]);// 没有自定义的,但有系统默认的UEH,就传给系统处理} else if (this.sysDefUeh != null) {BuglyLog.e("system handle start!", new Object[0]);this.sysDefUeh.uncaughtException(paramThread, paramThrowable);BuglyLog.e("system handle end!", new Object[0]);// 都没有,就自行结束进程} else {BuglyLog.e("crashreport last handle start!", new Object[0]);exit();BuglyLog.e("crashreport last handle end!", new Object[0]);}}}
}

bugly的实现完全符合标准流程,同时通过测试其执行日志,与上述代码中的日志输出也完全一致,Bugly 确实不会私吞异常

Bugly未捕获异常处理流程

Bugly未捕获异常处理流程.png

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...