如何捕获全局异常

作者: 开发者小王 | 来源:发表于2017-07-29 15:21 被阅读277次

    前言

    大家都知道,安装Android系统的手机版本和设备千差万别,在模拟器上运行良好的程序安装到某款手机上说不定就出现崩溃的现象,开发者个人不可能购买所有设备逐个调试。所以在程序发布出去之后,如果出现了崩溃现象,开发者应该及时获取在该设备上导致崩溃的信息,这对于下一个版本的bug修复帮助极大,所以今天就来介绍一下如何在程序崩溃的情况下收集相关的设备参数信息和具体的异常信息,并发送这些信息到服务器供开发者分析和调试程序。

    从制造异常开始

    为了演示,我们先制造一个异常。新建一个名为CrashDemo项目,在MainActivity中输入下面代码,故意制造了一个潜在的运行期异常,在一个null对象上调用方法。

    public class MainActivity extends AppCompatActivity {
        private String mStr;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            System.out.print(mStr.equals("print AnyThings!"));
        }
    }
    

    在测试机上发布,程序运行后直接crash了。

    遇到软件没有捕获的异常之后,系统会弹出这个默认的强制关闭对话框。我们当然不希望用户看到这种现象,简直是对用户心灵上的打击,而且对我们的bug的修复也是毫无帮助的。我们需要的是软件有一个全局的异常捕获器,当出现一个我们没有发现的异常时,捕获这个异常,并且将异常信息记录下来,上传到服务器供开发者这分析出现异常的具体原因。

    捕获全局异常的方法

    捕获全局异常主要是靠Thread类中的UncaughtExceptionHandler接口。

    public class Thread implements Runnable {
        // ,,,
    
        public interface UncaughtExceptionHandler {
            void uncaughtException(Thread t, Throwable e);
        }
    }
    

    该接口中仅有一个方法,是用来处理未捕获异常的,传入线程和一个可抛出的对象。当线程因未捕获的异常而要终止的时候会回掉这个接口中的uncaughtException方法。

    每个线程对象中有个UncaughtExceptionHandler类型的引用,提供了getter和setter方法,这是用策略模式解耦(定义一系列可替换的算法族,可用setter传入不同的算法)。

    public class Thread implements Runnable {
        // ,,,
    
        private ThreadGroup group;
        private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
    
        public UncaughtExceptionHandler getUncaughtExceptionHandler() {
            return uncaughtExceptionHandler != null ?
                uncaughtExceptionHandler : group;
        }
    
        public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
            checkAccess();
            uncaughtExceptionHandler = eh;
        }
    
        // Dispatch an uncaught exception to the handler. This method is
        // intended to be called only by the JVM.
        private void dispatchUncaughtException(Throwable e) {
            getUncaughtExceptionHandler().uncaughtException(this, e);
        }
    }
    

    dispatchUncaughtException方法只会被JVM调用,发布异常给handler。getUncaughtExceptionHandler方法将返回uncaughtExceptionHandler对象,而当uncaughtExceptionHandler为null时,也就客户没有设置策略的时候,就返回group对象。

    group是ThreadGroup类对象,顾名思义就是线程组的意思,在创建一个Thread对象时可以指定线程组。我们来看看这个类中有哪些方法(部分被标记为 @Deprecated的废弃方法被我去掉了)。

    public class ThreadGroup implements Thread.UncaughtExceptionHandler {
        static final ThreadGroup systemThreadGroup = new ThreadGroup();
        static final ThreadGroup mainThreadGroup = new ThreadGroup(systemThreadGroup, "main");
        private final ThreadGroup parent;
        String name;
        int maxPriority;
        boolean destroyed;
        boolean daemon;
        boolean vmAllowSuspension;
        int nUnstartedThreads = 0;
        int nthreads;
        Thread threads[];
        int ngroups;
        ThreadGroup groups[];
    
        private ThreadGroup();
        public ThreadGroup(String name);
        public ThreadGroup(ThreadGroup parent, String name);
        private ThreadGroup(Void unused, ThreadGroup parent, String name);
    
        private static Void checkParentAccess(ThreadGroup parent);
        public final String getName();
        public final ThreadGroup getParent();
        public final int getMaxPriority();
        public final boolean isDaemon();
        public synchronized boolean isDestroyed();
        public final void setDaemon(boolean daemon);
        public final void setMaxPriority(int pri);
        public final boolean parentOf(ThreadGroup g);
        public final void checkAccess();
        public int activeCount();
        public int enumerate(Thread list[]);
        public int enumerate(Thread list[], boolean recurse);
        private int enumerate(Thread list[], int n, boolean recurse);
        public int activeGroupCount();
        public int enumerate(ThreadGroup list[]);
        public int enumerate(ThreadGroup list[], boolean recurse);
        private int enumerate(ThreadGroup list[], int n, boolean recurse);
        public final void interrupt();
        private boolean stopOrSuspend(boolean suspend);
        public final void destroy();
        private final void add(ThreadGroup g);
        private void remove(ThreadGroup g);
        void addUnstarted();
        void add(Thread t)
        void threadStartFailed(Thread t);
        void threadTerminated(Thread t);
        private void remove(Thread t);
        public void list();
        void list(PrintStream out, int indent);
        public void uncaughtException(Thread t, Throwable e);
        public String toString();
    }
    

    通过源码,我们了解到ThreadGroup包含一个同样为ThreadGroup类型的父元素parent,也有Thread[]和ThreadGroup[]类型作为其子元素。这关系就像View和ViewGroup,Thread和ThreadGroup构成一棵线程树。

    不出意料ThreadGroup果然实现了UncaughtExceptionHandler接口,我们看看uncaughtException方法的实现。

    // ThreadGroup.java
    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
    

    可以看出ThreadGroup把异常传递给他线程树上的父亲来处理,当没父亲时将会调用线程类里的默认处理器来处理。这是一种责任链模式,儿子遇到无法解决的难题时就把难题交给他的父亲来处理,一直传递上去,如果最终也没找到能处理的父亲,就把异常交给默认处理器一把处理。

    Thread中有个属于类的defaultUncaughtExceptionHandler,也有对应的getter和setter方法,这就是线程类的默认处理器,传递链上的最后一棒。注意defaultUncaughtExceptionHandler的uncaughtException不能调用ViewGroup的uncaughtException放法,否则会无限递归。

    public class Thread implements Runnable {
        // ...
    
        private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
    
        public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
             defaultUncaughtExceptionHandler = eh;
         }
    
        public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
            return defaultUncaughtExceptionHandler;
        }
    }
    

    以上分析的流程可以用下面这张图来概括。

    当有未捕获的异常时,JVM会首先把异常分发给当前线程处理,若当前线程无法处理,则此异常会在链上传递。

    处理全局异常的Demo

    知道了这些原理,我们也就知道了该怎么捕获全局异常了。我们可以通过设置defaultUncaughtExceptionHandler来统一处理全局异常。下面贴出了Demo的源码。

    首先创建一个捕获处理全局异常的类CrashHandler,实现UncaughtExceptionHandler接口。该类设计为单例,完成错误信息的收集和上报,具体的逻辑结合代码中的注释不难看懂,这里我们把错误信息存储在本地,开发过程中可以把错误信息上报服务器。

    public class CrashHandler implements Thread.UncaughtExceptionHandler {
        private static final String TAG = "CrashHandler";
        private Context mContext;
        private static CrashHandler mInstance;
        private Thread.UncaughtExceptionHandler mDefaultHandler;
        // 用来存储设备信息和异常信息  
        private Map<String, String> mInfo = new HashMap<>();
        private DateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        private CrashHandler() {}
    
        public static CrashHandler getInstance() {
            if (mInstance == null) {
                synchronized (CrashHandler.class) {
                    if (mInstance == null) {
                        mInstance = new CrashHandler();
                    }
                }
            }
            return mInstance;
        }
    
        public void init(Context context) {
            mContext = context;
            // 获取系统默认的UncaughtException处理器  
            mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
            //设置该CrashHandler为程序的默认处理器  
            Thread.setDefaultUncaughtExceptionHandler(this);
        }
    
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            // 如果用户没有处理则让系统默认的异常处理器来处理  
            if (!handleException(e) && mDefaultHandler != null) {
                mDefaultHandler.uncaughtException(t, e);
            } else {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e1) {
                    Log.e(TAG, "error", e);  
                }
                // 退出程序  
                Process.killProcess(Process.myPid());  
                System.exit(1);
            }
        }
    
        // 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成. 
        private boolean handleException(Throwable e) {
            if (e == null) {
                return false;
            }
            // 使用Toast来显示异常信息  
            new Thread() {
                @Override
                public void run() {
                    Looper.prepare();
                    Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_SHORT).show();
                    Looper.loop();
                }
            }.start();
    
            //收集设备参数信息   
            collectErrorInfo();  
            //保存日志文件   
            saveErrorInfo(e);
            return true;
        }
    
        // 收集设备参数信息 
        private void collectErrorInfo() {
            PackageManager pm = mContext.getPackageManager();
            try {
                PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
                if (pi != null) {
                    String versionName = TextUtils.isEmpty(pi.versionName) ? "未设置版本号" : pi.versionName;
                    String versionCode = pi.versionCode + "";
                    mInfo.put("versionName", versionName);
                    mInfo.put("versionCode", versionCode);
                }
    
                Field[] fields = Build.class.getFields();
                if (fields != null && fields.length > 0) {
                    for (Field field : fields) {
                        field.setAccessible(true);
                        try {
                            mInfo.put(field.getName(), field.get(null).toString());
                        } catch (IllegalAccessException e) {
                            Log.e(TAG, "an error occured when collect crash info", e);  
                        }
                    }
                }
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "an error occured when collect package info", e);  
            }
        }
    
        // 保存错误信息到文件中 
        private void saveErrorInfo(Throwable e) {
            StringBuffer stringBuffer = new StringBuffer();
            for (Map.Entry<String, String> entry : mInfo.entrySet()) {
                String keyName = entry.getKey();
                String value = entry.getKey();
                stringBuffer.append(keyName+"="+value+"\n");
            }
    
            Writer writer = new StringWriter();
            PrintWriter printWriter = new PrintWriter(writer);
            e.printStackTrace(printWriter);
            Throwable cause = e.getCause();
            while (cause != null) {
                cause.printStackTrace(printWriter);
                cause = cause.getCause();
            }
    
            printWriter.close();
    
            String result = writer.toString();
            stringBuffer.append(result);
    
            long currentTime = System.currentTimeMillis();
            String time = mDateFormat.format(new Date());
            String fileName = "crash-" + time + "-" + currentTime + ".log";
    
            // 判断有没有SD卡
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                File dir = mContext.getExternalFilesDir("crash");
                if (!dir.exists()) {
                    dir.mkdirs();
                }
    
                FileOutputStream fos = null;
                try {
                    fos = new FileOutputStream(dir + "/" + fileName);
                    fos.write(stringBuffer.toString().getBytes());
                } catch (FileNotFoundException e1) {
                    Log.e(TAG, "an error occured due to file not found", e);  
                } catch (IOException e2) {
                    Log.e(TAG, "an error occured while writing file...", e);  
                } finally {
                    try {
                        fos.close();
                    } catch (IOException e1) {
                        Log.e(TAG, "an error occured when close file", e);  
                    }
                }
            }
        }
    }
    

    完成这个CrashHandler后,我们需要在一个Application环境中让其初始化,为此,我们继承android.app.Application,添加自己的代码,CrashApplication.java代码如下

    public class CrashApplication extends Application {
        private static Context mContext;
    
        @Override
        public void onCreate() {
            super.onCreate();
            this.mContext = this;
            CrashHandler.getInstance().init(this);
        }
    }
    

    最后在AndroidManifest.xml里将CrashApplication配置成运行的Application就可以了,这个非常简单就不贴代码了。

    测试

    运行程序,程序在弹出提示Toast之后就退出了,在Android/data/[包名]/files/crash目录下,我们找到了记录的异常信息。

    记录的异常信息。

    SUPPORTED_64_BIT_ABIS=SUPPORTED_64_BIT_ABIS
    versionCode=versionCode
    BOARD=BOARD
    BOOTLOADER=BOOTLOADER
    TYPE=TYPE
    ID=ID
    TIME=TIME
    BRAND=BRAND
    SERIAL=SERIAL
    HARDWARE=HARDWARE
    SUPPORTED_ABIS=SUPPORTED_ABIS
    CPU_ABI=CPU_ABI
    RADIO=RADIO
    IS_DEBUGGABLE=IS_DEBUGGABLE
    MANUFACTURER=MANUFACTURER
    SUPPORTED_32_BIT_ABIS=SUPPORTED_32_BIT_ABIS
    isOSUpgradeKK2LL=isOSUpgradeKK2LL
    TAGS=TAGS
    IS_SYSTEM_SECURE=IS_SYSTEM_SECURE
    CPU_ABI2=CPU_ABI2
    UNKNOWN=UNKNOWN
    USER=USER
    FINGERPRINT=FINGERPRINT
    FOTA_INFO=FOTA_INFO
    HOST=HOST
    PRODUCT=PRODUCT
    versionName=versionName
    DISPLAY=DISPLAY
    MODEL=MODEL
    DEVICE=DEVICE
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.sandemarine.crashdemo/com.sandemarine.crashdemo.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3319)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3415)
        at android.app.ActivityThread.access$1100(ActivityThread.java:229)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:7406)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
    Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
        at com.sandemarine.crashdemo.MainActivity.onCreate(MainActivity.java:14)
        at android.app.Activity.performCreate(Activity.java:6904)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1136)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3266)
        ... 9 more
    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference
        at com.sandemarine.crashdemo.MainActivity.onCreate(MainActivity.java:14)
        at android.app.Activity.performCreate(Activity.java:6904)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1136)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3266)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3415)
        at android.app.ActivityThread.access$1100(ActivityThread.java:229)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:7406)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
    

    相关文章

      网友评论

      • 飞翔的泥巴:while (cause != null) {
        cause.printStackTrace(printWriter);
        cause = cause.getCause();
        }
        这一段会死循环吧!
      • 小楠总:获取到的设备信息有问题,例如versionCode=versionCode
        开发者小王:@小楠总 感谢指出错误

      本文标题:如何捕获全局异常

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