如何捕获全局异常

前言

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

推荐阅读更多精彩内容

  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,438评论 1 15
  • 下面是我自己收集整理的Java线程相关的面试题,可以用它来好好准备面试。 参考文档:-《Java核心技术 卷一》-...
    阿呆变Geek阅读 14,735评论 14 507
  • 导读目录 线程组(ThreadGroup) 线程池(Thread Pool) Fork/Join框架和Execut...
    ql2012jz阅读 1,443评论 0 0
  • 文/慕蕤海 找个什么样的男人才不会失望?这大概是大多数女生想问的问题。 太有实力的,怕将来不待见自己;太不努力的,...
    穿花蛱蝶深深见阅读 606评论 10 6
  • 像往常一样,我在三点出门坐公交去学校,在等车时,听到了争吵声,不明白其中的缘由,便没有再留意。 直到那个男人走上了...
    肖大神的小跟班阅读 167评论 1 0