美文网首页
Android未捕获异常监控原理

Android未捕获异常监控原理

作者: 未子涵 | 来源:发表于2022-11-17 13:57 被阅读0次

    背景

    • 本文仅探讨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.将异常信息继续丢回给原有的UncaughtExceptionHandler
            if (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.
        // 分发给 UncaughtExceptionPreHandler
        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
            }
        }
        // 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;
    
        @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 {
                // 大部分情况都会走到这里
                logUncaught(t.getName(), ActivityThread.currentProcessName(), Process.myPid(), e);
            }
        }
    }
    
    public static void logUncaught(String threadName, String processName, int pid, Throwable e) {
        // 生成一段msg
        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(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);
        }
    
        @Override
        public 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 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.
                }
            }
        }
    }
    

    最后,让我们再多看一眼 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
    LoggingHandler KillApplicationHandler
    注册为 pre-handler handler
    可替换 仅可被系统使用,不能被替换 可被应用替换
    作用 在 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 失效,影响自己内部调试。

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

    更可靠的推测

    <font color='red'>崩溃处理流程中的一切异步操作,均有失败的风险</font>

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

    • 上传失败:数据安全,下次启动时会上传
    • 缓存DB失败:<font color='red'>数据丢失</font>,本次/下次均不会上传

    因此,<font color='red'>至少要将“缓存DB”改为同步操作,才能保证缓存成功</font>。

    问题: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 = true
        if (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;
            }
            // 没有设置StrategyBean
            if (!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 = true
            if (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

    相关文章

      网友评论

          本文标题:Android未捕获异常监控原理

          本文链接:https://www.haomeiwen.com/subject/ejdpxdtx.html