Android Crash的防護(hù)與追蹤

一. 序

Android系統(tǒng)中,拋出Exception 或者 Error都會(huì)導(dǎo)致Crash.進(jìn)而導(dǎo)致App強(qiáng)制退出.簡(jiǎn)單的來(lái)說(shuō)就是因?yàn)閽伋霎惓5拇a.并未被Try catch包圍..就會(huì)導(dǎo)致進(jìn)程被殺.

二. 原理

從Fork進(jìn)程伊始,就已經(jīng)存在的UncaughtExceptionHandler(大致描述了AMS對(duì)于異常處理的過(guò)程.).

1. 進(jìn)程Fork之后就注冊(cè)了一個(gè)UncaughtHandler

//RuntimeInit.java中的zygoteInit函數(shù)
public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
        throws ZygoteInit.MethodAndArgsCaller {
    ............
    //跟進(jìn)commonInit
    commonInit();
    ............
}
private static final void commonInit() {
    ...........
    /* set default handler; this applies to all threads in the VM */
    //到達(dá)目的地!
    Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
    ...........
}

2. 異常處理

當(dāng)UncaughtHandler接收到未捕獲異常的時(shí)候.進(jìn)程會(huì)自殺,并且彈出大家最熟悉不過(guò)的Force Close對(duì)話(huà)框.

private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        try {
            // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
            if (mCrashing) return;
            mCrashing = true;

            if (mApplicationObject == null) {
                Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
            } else {
                //打印進(jìn)程的crash信息
                .............
            }
            .............
            // Bring up crash dialog, wait for it to be dismissed
            //調(diào)用AMS的接口,進(jìn)行處理
            ActivityManagerNative.getDefault().handleApplicationCrash(
                    mApplicationObject, new ApplicationErrorReport.CrashInfo(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.
            //crash的最后,會(huì)殺死進(jìn)程
            Process.killProcess(Process.myPid());
            //并exit
            System.exit(10);
        }
    }
}

又發(fā)現(xiàn)了神秘的System.exit(10);這里面的魔法數(shù)字.
Difference in System. exit(MagicCode) in Java

3.UncaughtExceptionHandler

3.1 簡(jiǎn)單說(shuō)說(shuō)UncaughtExceptionHandler

UncaughtExceptionHandler存在于Thread中.當(dāng)異常發(fā)生且未捕獲時(shí).異常會(huì)透過(guò)UncaughtExceptionHandler拋出.并且該線(xiàn)程會(huì)消亡.所以在Android中子線(xiàn)程死亡是允許的.主線(xiàn)程死亡就會(huì)導(dǎo)致ANR.

下面是相關(guān)源碼的截取.仔細(xì)閱讀會(huì)發(fā)現(xiàn).Thread中存在兩個(gè)UncaughtExceptionHandler.一個(gè)是靜態(tài)的defaultUncaughtExceptionHandler,另一個(gè)是非靜態(tài)uncaughtExceptionHandler.

  • defaultUncaughtExceptionHandler:設(shè)置一個(gè)靜態(tài)的默認(rèn)的UncaughtExceptionHandler.來(lái)自所有線(xiàn)程中的Exception在拋出,并且為捕獲的情況下.都會(huì)從此路過(guò).大家可以看到進(jìn)程fork的時(shí)候設(shè)置的就是這個(gè)靜態(tài)的defaultUncaughtExceptionHandler.管轄范圍為整個(gè)進(jìn)程.
  • uncaughtExceptionHandler:為單個(gè)線(xiàn)程設(shè)置一個(gè).屬于線(xiàn)程自己的uncaughtExceptionHandler.也就是說(shuō).他的管轄范圍比較小.
public class Thread implements Runnable {

 ...........
 
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        void uncaughtException(Thread t, Throwable e);
    }

    // null unless explicitly set
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

  
    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
         defaultUncaughtExceptionHandler = eh;
     }
    
    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        checkAccess();
        uncaughtExceptionHandler = eh;
    }
    ...
}

3.2 UncaughtExceptionHandler的"職責(zé)鏈"

  1. 當(dāng)我們自定義一個(gè)CrashHandlerregister(),本質(zhì)上這個(gè)CrashHandler就已經(jīng)持有進(jìn)程中上一個(gè)注冊(cè)成DefaultUncaughtExceptionHandler的引用..并且將自己設(shè)置成進(jìn)程中DefaultUncaughtExceptionHandler.

  2. 異常來(lái)了.我們先在uncaughtException中處理,如果不攔截.就包裝一些擴(kuò)展信息,并且交給我這持有的引用mUncaughtExceptionHandler繼續(xù)處理.

  3. 大家可能看出來(lái)了.這是一個(gè)鏈?zhǔn)降慕Y(jié)構(gòu).直到丟給最后進(jìn)程中的UncaughtExceptionHandler.然后就ForceClose了.

public class CrashHandler implements Thread.UncaughtExceptionHandler {

  private Thread.UncaughtExceptionHandler mUncaughtExceptionHandler;

  ......

  void register() {
        mUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
    }
  }
  
  
   @Override public void uncaughtException(Thread thread, Throwable throwable) {
    
    //做些事情
    ......
    
    if(心情不好){
        return;
    }
    //心情不好的話(huà),異常就不能繼續(xù)傳遞了.
    mUncaughtExceptionHandler.uncaughtException(thread, facadeThrowable);
  }

  ......
}

總結(jié):很重要!很重要!!很重要!!!

UncaughtExceptionHandler是以鏈?zhǔn)浇Y(jié)構(gòu)存在.原則上,誰(shuí)后注冊(cè)的,誰(shuí)優(yōu)先處理異常,并且決定,這個(gè)異常是否交給上一個(gè)注冊(cè)的.這點(diǎn)很重要,牢記!假如我們注冊(cè)在其他UncaughtExceptionHandler后邊很有可能導(dǎo)致,因?yàn)樗麄儾⑽蠢^續(xù)傳遞Exception.導(dǎo)致一些其他問(wèn)題.所以我們要注冊(cè)在最后.以便優(yōu)先處理.

三. App層可以做的Crash防護(hù)

1.Crash統(tǒng)計(jì)平臺(tái)

例如Fabric等錯(cuò)誤日志上報(bào)平臺(tái),可以當(dāng)Crash發(fā)生時(shí),收集異常信息.到平臺(tái),此處不擴(kuò)展講.網(wǎng)上相關(guān)文檔很多.本質(zhì)也是注冊(cè)一個(gè)UncaughtExceptionHandler然后將Throw上報(bào)給服務(wù)器.后續(xù)介紹都會(huì)以Fabric為例說(shuō)明.

2. try-catch大法好.

Java的異常處理可以讓程序具有更好的容錯(cuò)性,程序更加健壯。當(dāng)程序運(yùn)行出現(xiàn)意外時(shí),系統(tǒng)會(huì)自動(dòng)生成一個(gè)Exception對(duì)象來(lái)通知程序。大家肯定會(huì)考慮到性能損耗問(wèn)題。畢竟做了“額外”的事情。這里我從兩種方式去探究一下:
寫(xiě)兩個(gè)一樣邏輯的函數(shù),只不過(guò)一個(gè)包含try-catch代碼塊,一個(gè)不包含,分別循環(huán)調(diào)用百萬(wàn)次,通過(guò)System.nanoTime()來(lái)比較兩個(gè)函數(shù)百萬(wàn)次調(diào)用的耗時(shí)。本機(jī)跑了一下基本上沒(méi)什么區(qū)別。
可以看看.java文件經(jīng)過(guò)編譯生成的JVM可以執(zhí)行的.class文件里的字節(jié)碼指令。

 javap -verbose ReturnValueTest  xx.class 命令可以查看字節(jié)碼

《深入Java虛擬機(jī)》作者Bill Venners于1997年所寫(xiě)的文章How the Java virtual machine handles exceptions比較詳盡地分析了一番。文章從反編譯出的指令發(fā)現(xiàn)加了try-catch塊的代碼跟沒(méi)有加的代碼運(yùn)行時(shí)的指令是完全一致的(你也可以按照上面命令自行進(jìn)行對(duì)比)。 如果程序運(yùn)行過(guò)程中不產(chǎn)生異常的話(huà)try catch 幾乎是不會(huì)對(duì)運(yùn)行產(chǎn)生任何影響的。只是在產(chǎn)生異常的時(shí)候jvm會(huì)追溯異常調(diào)用棧。這部分耗時(shí)就相對(duì)較高了。

3.上文地提到的"職責(zé)連"

我們能做的就是在所有第三方的UncaughtExceptionHandler注冊(cè)之后,注冊(cè)一個(gè)自己的CrashHandler。這樣我們就可以在第一時(shí)間接收到異常之后。做異常攔截或者異常包裝。

4.異常攔截

上文同樣提到了。我們注冊(cè)一個(gè)自己的CrashHandler的目的之一,就是優(yōu)先與異常見(jiàn)面。當(dāng)發(fā)現(xiàn)可以攔截的異常的時(shí)候。就不將其繼續(xù)傳遞.異常攔截 最重要的原則 就是不能攔截主線(xiàn)程中的異常:

這是一段異常攔截的代碼:

  @Override public void uncaughtException(Thread thread, Throwable throwable) {
    //先嘗試攔截?cái)r截
    if (crashInterceptor()) {
      return;
    }
    uncaughtExceptionHandler.uncaughtException(thread, facadeThrowable);
  }
 //異常攔截器
public static boolean crashInterceptor(Throwable throwable, Thread thread) {

    if (thread.getId() == 1
        || throwable == null
        || throwable.getMessage() == null
        || throwable.getStackTrace() == null) {
      //異常發(fā)生之后,所在線(xiàn)程會(huì)掛掉.所以主線(xiàn)程異常,攔截了也沒(méi)用.主線(xiàn)程也會(huì)死掉.
      //除非,后續(xù)判斷,app在前臺(tái),觸發(fā)APP重啟.
      return false;
    }

    String classpath = null;
    if (throwable.getStackTrace() != null && throwable.getStackTrace().length > 0) {
      classpath = throwable.getStackTrace()[0].toString();
    }

    if (classpath == null) {
      return false;
    }

    //攔截GMS異常.
    if (throwable.getMessage().contains("Results have already been set") && classpath.contains(
        "com.google.android.gms")) {
      logException(throwable);
      return true;
    }

    //攔截GMS的 NPE.
    if (classpath.contains("com.google.android.gms") && throwable instanceof NullPointerException) {
      CrashHelper.logException(CrashFacade.facadeThrowable(throwable));
      return true;
    }


    //攔截ssl_NPE
    if (throwable instanceof NullPointerException && throwable.getMessage()
        .contains("ssl_session == null")) {
      CrashHelper.logException(CrashFacade.facadeThrowable(throwable));
      return true;
    }

    return false;
  }

5.異常信息包裝上傳

當(dāng)我們用了平臺(tái)之后,發(fā)現(xiàn)除了我們自己能看到的又明確調(diào)用棧的異常信息。還有許許多多看不到調(diào)用棧的。或者是第三方SDK里的Crash.這些Crash因?yàn)闆](méi)有調(diào)用棧。一直是個(gè)很頭疼的問(wèn)題。

此處我們追加的部分信息的截圖.兩個(gè)例子.沒(méi)有調(diào)用棧的情況.
這是我在StackOverFlow上和Google Issue Tracker上的提問(wèn)
StackOverFlow : GMS IllegalStateException : Results have already been set?
Google Issue Tracker : GMS Results have already been set

  • GMS IllegalStateException
  • finalize() timedout after 10 seconds

追加擴(kuò)展信息代碼如下:

  @Override public void uncaughtException(Thread thread, Throwable throwable) {
    //先嘗試攔截?cái)r截
    if (crashInterceptor()) {
      return;
    }
    //再包裝擴(kuò)展信息,交給Fabric上報(bào)服務(wù)器
    Throwable facadeThrowable = facadeThrowable(throwable , "<HelloWorld>");
    uncaughtExceptionHandler.uncaughtException(thread, facadeThrowable);
  }
  //通過(guò)反射,在detailMessage后面追加信息
  public static Throwable facadeThrowable(Throwable throwable , String facadeMessage) {
    try {
      Field field = getDeclaredField(throwable, "detailMessage");
      if (field == null) {
        return throwable;
      }
      field.setAccessible(true);
      String originDetailMessage = (String) field.get(throwable);
      String newDetailMessage = originDetailMessage + facadeMessage;
      field.set(throwable, newDetailMessage);
    } catch (Exception ignore) {
      CrashHelper.logExceptionWithoutFacade(ignore);
    }
    return throwable;
  }

我這里都是先通過(guò)包裝Crash.收集沒(méi)有調(diào)用棧信息異常和第三方庫(kù)的異常的所在線(xiàn)程.在謹(jǐn)慎的增加對(duì)應(yīng)的異常攔截.確保沒(méi)有在主線(xiàn)程中攔截異常..畢竟ANR了..也不合適.就直接掛掉吧...所以先收集包裝信息.再?zèng)Q定攔截哪些異常

WARNING:之前嘗試.在UnCaughtHandler中,將Exception放到一個(gè)new Throwable()的cause中.并追加信息.這種方式會(huì)導(dǎo)致平臺(tái)日志堆疊,因?yàn)閚ew Throwable都產(chǎn)生在同樣的地方.平臺(tái)會(huì)把日志合并.所以才考慮用反射的方法加到detailMessage后面的.
Fabric會(huì)給與

  • Crash堆疊在了一起
  • 調(diào)用棧都跑到我的uncaughtException里了.

大結(jié)局

  • finalize() timedout after 10 seconds

哦?FinalizerWatchdogDaemon是什么線(xiàn)程?
引發(fā)了我研究從Daemons到finalize timed out after 10 seconds這個(gè)問(wèn)題

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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