Android Crash的防护与追踪

一. 序

Android系统中,抛出Exception 或者 Error都会导致Crash.进而导致App强制退出.简单的来说就是因为抛出异常的代码.并未被Try catch包围..就会导致进程被杀.

二. 原理

从Fork进程伊始,就已经存在的UncaughtExceptionHandler(大致描述了AMS对于异常处理的过程.).

1. 进程Fork之后就注册了一个UncaughtHandler

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

2. 异常处理

当UncaughtHandler接收到未捕获异常的时候.进程会自杀,并且弹出大家最熟悉不过的Force Close对话框.

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 {
                //打印进程的crash信息
                .............
            }
            .............
            // Bring up crash dialog, wait for it to be dismissed
            //调用AMS的接口,进行处理
            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的最后,会杀死进程
            Process.killProcess(Process.myPid());
            //并exit
            System.exit(10);
        }
    }
}

又发现了神秘的System.exit(10);这里面的魔法数字.
Difference in System. exit(MagicCode) in Java

3.UncaughtExceptionHandler

3.1 简单说说UncaughtExceptionHandler

UncaughtExceptionHandler存在于Thread中.当异常发生且未捕获时.异常会透过UncaughtExceptionHandler抛出.并且该线程会消亡.所以在Android中子线程死亡是允许的.主线程死亡就会导致ANR.

下面是相关源码的截取.仔细阅读会发现.Thread中存在两个UncaughtExceptionHandler.一个是静态的defaultUncaughtExceptionHandler,另一个是非静态uncaughtExceptionHandler.

  • defaultUncaughtExceptionHandler:设置一个静态的默认的UncaughtExceptionHandler.来自所有线程中的Exception在抛出,并且为捕获的情况下.都会从此路过.大家可以看到进程fork的时候设置的就是这个静态的defaultUncaughtExceptionHandler.管辖范围为整个进程.
  • uncaughtExceptionHandler:为单个线程设置一个.属于线程自己的uncaughtExceptionHandler.也就是说.他的管辖范围比较小.
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的"职责链"

  1. 当我们自定义一个CrashHandlerregister(),本质上这个CrashHandler就已经持有进程中上一个注册成DefaultUncaughtExceptionHandler的引用..并且将自己设置成进程中DefaultUncaughtExceptionHandler.

  2. 异常来了.我们先在uncaughtException中处理,如果不拦截.就包装一些扩展信息,并且交给我这持有的引用mUncaughtExceptionHandler继续处理.

  3. 大家可能看出来了.这是一个链式的结构.直到丢给最后进程中的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;
    }
    //心情不好的话,异常就不能继续传递了.
    mUncaughtExceptionHandler.uncaughtException(thread, facadeThrowable);
  }

  ......
}

总结:很重要!很重要!!很重要!!!

UncaughtExceptionHandler是以链式结构存在.原则上,谁后注册的,谁优先处理异常,并且决定,这个异常是否交给上一个注册的.这点很重要,牢记!假如我们注册在其他UncaughtExceptionHandler后边很有可能导致,因为他们并未继续传递Exception.导致一些其他问题.所以我们要注册在最后.以便优先处理.

三. App层可以做的Crash防护

1.Crash统计平台

例如Fabric等错误日志上报平台,可以当Crash发生时,收集异常信息.到平台,此处不扩展讲.网上相关文档很多.本质也是注册一个UncaughtExceptionHandler然后将Throw上报给服务器.后续介绍都会以Fabric为例说明.

2. try-catch大法好.

Java的异常处理可以让程序具有更好的容错性,程序更加健壮。当程序运行出现意外时,系统会自动生成一个Exception对象来通知程序。大家肯定会考虑到性能损耗问题。毕竟做了“额外”的事情。这里我从两种方式去探究一下:
写两个一样逻辑的函数,只不过一个包含try-catch代码块,一个不包含,分别循环调用百万次,通过System.nanoTime()来比较两个函数百万次调用的耗时。本机跑了一下基本上没什么区别。
可以看看.java文件经过编译生成的JVM可以执行的.class文件里的字节码指令。

 javap -verbose ReturnValueTest  xx.class 命令可以查看字节码

《深入Java虚拟机》作者Bill Venners于1997年所写的文章How the Java virtual machine handles exceptions比较详尽地分析了一番。文章从反编译出的指令发现加了try-catch块的代码跟没有加的代码运行时的指令是完全一致的(你也可以按照上面命令自行进行对比)。 如果程序运行过程中不产生异常的话try catch 几乎是不会对运行产生任何影响的。只是在产生异常的时候jvm会追溯异常调用栈。这部分耗时就相对较高了。

3.上文地提到的"职责连"

我们能做的就是在所有第三方的UncaughtExceptionHandler注册之后,注册一个自己的CrashHandler。这样我们就可以在第一时间接收到异常之后。做异常拦截或者异常包装。

4.异常拦截

上文同样提到了。我们注册一个自己的CrashHandler的目的之一,就是优先与异常见面。当发现可以拦截的异常的时候。就不将其继续传递.异常拦截 最重要的原则 就是不能拦截主线程中的异常:

这是一段异常拦截的代码:

  @Override public void uncaughtException(Thread thread, Throwable throwable) {
    //先尝试拦截拦截
    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) {
      //异常发生之后,所在线程会挂掉.所以主线程异常,拦截了也没用.主线程也会死掉.
      //除非,后续判断,app在前台,触发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.异常信息包装上传

当我们用了平台之后,发现除了我们自己能看到的又明确调用栈的异常信息。还有许许多多看不到调用栈的。或者是第三方SDK里的Crash.这些Crash因为没有调用栈。一直是个很头疼的问题。

此处我们追加的部分信息的截图.两个例子.没有调用栈的情况.
这是我在StackOverFlow上和Google Issue Tracker上的提问
StackOverFlow : GMS IllegalStateException : Results have already been set?
Google Issue Tracker : GMS Results have already been set

  • GMS IllegalStateException
  • finalize() timedout after 10 seconds

追加扩展信息代码如下:

  @Override public void uncaughtException(Thread thread, Throwable throwable) {
    //先尝试拦截拦截
    if (crashInterceptor()) {
      return;
    }
    //再包装扩展信息,交给Fabric上报服务器
    Throwable facadeThrowable = facadeThrowable(throwable , "<HelloWorld>");
    uncaughtExceptionHandler.uncaughtException(thread, facadeThrowable);
  }
  //通过反射,在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;
  }

我这里都是先通过包装Crash.收集没有调用栈信息异常和第三方库的异常的所在线程.在谨慎的增加对应的异常拦截.确保没有在主线程中拦截异常..毕竟ANR了..也不合适.就直接挂掉吧...所以先收集包装信息.再决定拦截哪些异常

WARNING:之前尝试.在UnCaughtHandler中,将Exception放到一个new Throwable()的cause中.并追加信息.这种方式会导致平台日志堆叠,因为new Throwable都产生在同样的地方.平台会把日志合并.所以才考虑用反射的方法加到detailMessage后面的.
Fabric会给与

  • Crash堆叠在了一起
  • 调用栈都跑到我的uncaughtException里了.

大结局

  • finalize() timedout after 10 seconds

哦?FinalizerWatchdogDaemon是什么线程?
引发了我研究从Daemons到finalize timed out after 10 seconds这个问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容