美文网首页Android Tips移动开发Android
Android Crash的防护与追踪

Android Crash的防护与追踪

作者: Jamin_正宗红罐辣酱 | 来源:发表于2017-12-21 19:52 被阅读125次

    一. 序

    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这个问题

    相关文章

      网友评论

        本文标题:Android Crash的防护与追踪

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