Android未捕獲異常監(jiān)控原理

背景

  • 本文僅探討java層的未捕獲異常的監(jiān)控
  • 為什么我們自己的異常捕獲總是比 Bugly 收到的信息少?

Android未捕獲異常的監(jiān)控與收集

Java層未捕獲異常監(jiān)控的基本實(shí)現(xiàn)

先看看Java層未捕獲異常監(jiān)控的運(yùn)行過程:

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實(shí)例
            oriHandler = Thread.getDefaultUncaughtExceptionHandler();
            // 2.設(shè)置自定義的UncaughtExceptionHandler實(shí)例,替代原有的
            Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        }
    }

    private MyUncaughtExceptionHandler() {}

    public void uncaughtException(Thread thread, Throwable ex) {
        // 3.收到未捕獲異常時(shí),進(jìn)行相應(yīng)的業(yè)務(wù)處理
        handleCrash(ex);
        // 4.將異常信息繼續(xù)丟回給原有的UncaughtExceptionHandler
        if (oriHandler != null) {
            oriHandler.uncaughtException(thread, ex);
        }
    }
}

提問:有沒有可能某些第三方SDK甚至應(yīng)用自己私吞異常,不向下傳遞?
對(duì)于這個(gè)猜測(cè)的證偽,需要從源碼層面分析,下面來看看。

Framework 對(duì)未捕獲異常的處理流程分析

應(yīng)用啟動(dòng)流程中會(huì)執(zhí)行到 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 的實(shí)現(xiàn)類
    // UncaughtExceptionPreHandler 無法被應(yīng)用替換,只有系統(tǒng)能用
    LoggingHandler loggingHandler = new LoggingHandler();
    RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
    // defaultUncaughtExceptionHandler 是可以被應(yīng)用替換掉的
    Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
    ...
}

可見,系統(tǒng)在應(yīng)用啟動(dòng)時(shí),同時(shí)注冊(cè)了 LoggingHandler 與 KillApplicationHandler 兩個(gè) UncaughtExceptionHandler 的實(shí)現(xiàn)類,用于監(jiān)聽未捕獲異常。為什么這么做呢,我們分別分析兩個(gè)實(shí)現(xiàn)類的流程:

先分析 RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler) :

// RuntimeHooks.java
public static void setUncaughtExceptionPreHandler(
        Thread.UncaughtExceptionHandler uncaughtExceptionHandler) {
        // 直接調(diào)用Thread.setUncaughtExceptionPreHandler()
    Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler);
}
// Thread.java
// Thread 同時(shí)提供了UncaughtExceptionPreHandler的set/get方法
public static void setUncaughtExceptionPreHandler(UncaughtExceptionHandler eh) {
    uncaughtExceptionPreHandler = eh;
}

// getUncaughtExceptionPreHandler() 方法會(huì)被 Thread.dispatchUncaughtException(Throwable e) 調(diào)用
public static UncaughtExceptionHandler getUncaughtExceptionPreHandler() {
    return uncaughtExceptionPreHandler;
}

// 分發(fā)異常
public final void dispatchUncaughtException(Throwable e) {
    // BEGIN Android-added: uncaughtExceptionPreHandler for use by platform.
    // 分發(fā)給 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.
    // 分發(fā)給 UncaughtExceptionHandler(就是應(yīng)用可以自行注冊(cè)的UEH)
    getUncaughtExceptionHandler().uncaughtException(this, e);
}

可見,系統(tǒng)會(huì)將未捕獲異常同時(shí)分發(fā)給 uncaughtExceptionPreHandler 和 UncaughtExceptionHandler。所以,接下來就要弄明白這兩個(gè) UEH 各自做了什么,先看 LoggingHandler :

// RuntimeInit.java
// LoggingHandler 是 RuntimeInit 的內(nèi)部類
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 {
            // 大部分情況都會(huì)走到這里
            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 在收到異?;卣{(diào)時(shí),僅僅做了一些系統(tǒng)打印操作。那么,KillApplicationHandler 呢?

// RuntimeInit.java
// KillApplicationHandler 是 RuntimeInit 的內(nèi)部類
/**
 * 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() 被觸發(fā)
    private final LoggingHandler mLoggingHandler;

    public KillApplicationHandler(LoggingHandler loggingHandler) {
        this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        try {
            // 確保 LoggingHandler.uncaughtException() 被觸發(fā)
            ensureLogging(t, e);

            // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
            if (mCrashing) return;
            mCrashing = true;

            /*
             * 如果正在使用 profiler 跟蹤代碼,就應(yīng)該停止 profiling,否則殺死應(yīng)用進(jìn)程時(shí)會(huì)導(dǎo)致 
             * profiler 的內(nèi)存數(shù)據(jù)丟失,而正確的 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) {
                // 如果應(yīng)用的UEH攔截了異常,不傳回給系統(tǒng),主要有影響的其實(shí)就是這里
                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 {
            // 最終結(jié)束進(jìn)程
            // 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.
            }
        }
    }
}

最后,讓我們?cè)俣嗫匆谎?ActivityThread.currentActivityThread().stopProfiling() ,確認(rèn)一下它到底是不是如推測(cè)的那樣,是控制 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 的內(nèi)部類
static final class Profiler {
    ...
    public void stopProfiling() {
        if (profiling) {
            profiling = false;
            // traceview 等工具做代碼跟蹤時(shí),都會(huì)用到的方法
            Debug.stopMethodTracing();
            if (profileFd != null) {
                try {
                    profileFd.close();
                } catch (IOException e) {
                }
            }
            profileFd = null;
            profileFile = null;
        }
    }
}

至此,F(xiàn)ramework 處理未捕獲異常的完整流程就分析完了。

最后做個(gè)總結(jié)
  • 系統(tǒng)會(huì)同時(shí)注冊(cè)兩個(gè) UncaughtExceptionHandler 實(shí)例:LoggingHandler 和 KillApplicationHandler
LoggingHandler KillApplicationHandler
注冊(cè)為 pre-handler handler
可替換 僅可被系統(tǒng)使用,不能被替換 可被應(yīng)用替換
作用 在 Logcat 中輸出崩潰日志 1.確保 LoggingHandler 被觸發(fā)
2.停止 Profiler(如果正在 profiling 的話)
3.彈出崩潰提示框
4.結(jié)束進(jìn)程

完整的未捕獲異常處理流程

Android未捕獲異常處理流程.png

回到最初的“猜測(cè)”

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

  • 異常的傳遞是必須的,最終要傳回給系統(tǒng)進(jìn)行處理,如果私吞,應(yīng)用會(huì)停留在前臺(tái),但無法響應(yīng)任何操作,這不符合大部分產(chǎn)品的設(shè)計(jì)理念

當(dāng)然,你可以私吞異常,并且自行殺死進(jìn)程,以避免應(yīng)用停留前臺(tái),如下操作:

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

但作為sdk,私吞異常還會(huì)導(dǎo)致應(yīng)用自己的UEH也捕獲不到異常,會(huì)被應(yīng)用投訴,得不償失!而作為應(yīng)用,如此操作會(huì)導(dǎo)致無法使用第三方異常監(jiān)控工具,也會(huì)導(dǎo)致 profiler 失效,影響自己內(nèi)部調(diào)試。

所以說,私吞異常是一個(gè)損人不利己的行為,甚至是害人害己。

更可靠的推測(cè)

<font color='red'>崩潰處理流程中的一切異步操作,均有失敗的風(fēng)險(xiǎn)</font>

新的優(yōu)化方向:異步 → 同步

  • 上傳失?。簲?shù)據(jù)安全,下次啟動(dòng)時(shí)會(huì)上傳
  • 緩存DB失敗:<font color='red'>數(shù)據(jù)丟失</font>,本次/下次均不會(huì)上傳

因此,<font color='red'>至少要將“緩存DB”改為同步操作,才能保證緩存成功</font>。

問題:uncaughtException()是在主線程觸發(fā)的,而我們禁止主線程I/O?

Bugly 源碼分析

  • crashreport:4.1.9

反編譯bugly,驗(yàn)證其異常系統(tǒng)的核心實(shí)現(xiàn):

// com.tencent.bugly.proguard.av.java
/**
 * 注冊(cè)監(jiān)控
 */
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();
        // 注冊(cè)過的Bugly監(jiān)控不再注冊(cè),避免重復(fù)注冊(cè)
        if (str1.equals(str2)) {
            return;
        }
        // 當(dāng)前的UEH是系統(tǒng)默認(rèn)的,緩存起來(系統(tǒng)默認(rèn)的在高版本中是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 {
            // 當(dāng)前的UEH是應(yīng)用自定義的
            BuglyLog.a("backup java handler: %s", new Object[]{uncaughtExceptionHandler.toString()});
            this.custUeh = uncaughtExceptionHandler;
        }
    }
    // 設(shè)置Bugly的UEH實(shí)例,替代原有的
    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]);
            // 有系統(tǒng)默認(rèn)的UEH,交給系統(tǒng)處理
            if (this.sysDefUeh != null) {
                BuglyLog.a("call system handler", new Object[0]);
                this.sysDefUeh.uncaughtException(paramThread, paramThrowable);
            } else {
                // 自行結(jié)束進(jìn)程
                exit();
            }
        }
    } else {
        BuglyLog.e("Java Catch Happen", new Object[0]);
    }
    // 核心處理
    try {
        // 監(jiān)控被禁用,結(jié)束
        if (!this.enable) {
            BuglyLog.c("Java crash handler is disable. Just return.", new Object[0]);
            return;
        }
        // 沒有設(shè)置StrategyBean
        if (!this.c.b()) {
            BuglyLog.d("no remote but still store!", new Object[0]);
        }
        // crash report被禁用,且設(shè)置了StrategyBean,則本地打印crash日志,結(jié)束
        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;
        // 打包c(diǎn)rash數(shù)據(jù),生成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);
        // 保存到本地?cái)?shù)據(jù)庫,并返回操作結(jié)果(推斷如果失敗,不再執(zhí)行上傳)
        if (!this.b.saveDb(crashDetailBean, uncaughtException)) {
            // 根據(jù)BuglyStrategy的設(shè)置,決定是否要立即上傳
            this.b.uploadOnNecessary(crashDetailBean, uncaughtException);
        }
        if (uncaughtException) {
            // 內(nèi)部只是一個(gè)日志打印的邏輯
            this.b.a(crashDetailBean);
        }
    } catch (Throwable throwable) {
        if (!BuglyLog.a((Throwable) (paramString = null))) {
            paramString.printStackTrace();
        }
    } finally {
        // uncaughtException = true
        if (uncaughtException) {
            // 有應(yīng)用自定義的UEH,且 它沒有在處理當(dāng)前異常,就傳給它處理
            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]);
            // 沒有自定義的,但有系統(tǒng)默認(rèn)的UEH,就傳給系統(tǒng)處理
            } 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]);
            // 都沒有,就自行結(jié)束進(jìn)程
            } else {
                BuglyLog.e("crashreport last handle start!", new Object[0]);
                exit();
                BuglyLog.e("crashreport last handle end!", new Object[0]);
            }
        }
    }
}

bugly的實(shí)現(xiàn)完全符合標(biāo)準(zhǔn)流程,同時(shí)通過測(cè)試其執(zhí)行日志,與上述代碼中的日志輸出也完全一致,Bugly 確實(shí)不會(huì)私吞異常。

Bugly未捕獲異常處理流程

Bugly未捕獲異常處理流程.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、背景 無論是Java還是Android項(xiàng)目,往往都會(huì)用到多線程。不管是主線程還是子線程,在運(yùn)行過程中,都有可能...
    ModestStorm閱讀 863評(píng)論 0 0
  • Android系統(tǒng)碎片化造成應(yīng)用程序崩潰嚴(yán)重,在模擬器上運(yùn)行良好的程序安裝到某款手機(jī)上說不定就會(huì)出現(xiàn)崩潰的現(xiàn)象。而...
    YoungTa0閱讀 18,070評(píng)論 5 20
  • 做為程序員,最不愿意看到的就是自己寫的程序崩潰,特別是遇到?jīng)]有錯(cuò)誤信息的崩潰的時(shí)候,往往程序員自己也就隨之一起崩潰...
    點(diǎn)融黑幫閱讀 2,104評(píng)論 2 2
  • 前提 今天在群里聊天的時(shí)候有群友問如何捕獲錯(cuò)誤日志,我說可以自己寫,也可以用第三方的比如騰訊的bugly,友盟的錯(cuò)...
    Silence瀟湘夜雨閱讀 1,878評(píng)論 2 12
  • 1. 概述 本文主要講解如何自定義 Android 全局異常捕獲,以及如何通過 Dialog 展示異常信息并將異常...
    極速24號(hào)閱讀 2,864評(píng)論 0 21

友情鏈接更多精彩內(nèi)容