手写CrashHandler实现UncaughtExceptionHandler拦截android异常

手写CrashHandler实现UncaughtExceptionHandler拦截android异常

作者:码字员小D

有点复杂,虽然知道原理,但是并不好从哪开始写了。。。。。。

首先这是个需要在整个app运行状态中都需要存在的对象,所以需要在application里初始化这个类,并且这个类实例~~~慢着!发现这里代码有疑问,application中只在oncreate方法里面初始化

public class CrashApplication extends Application {
@Override
public void onCreate() {
    super.onCreate();
//      CrashHandler crashHandler = CrashHandler.getInstance();
    CrashHandler crashHandler = new CrashHandler();
    crashHandler.init(getApplicationContext());
}
}

在application里面并没有持有这个UncaughtExceptionHandler的实例(CrashHandler实现了UncaughtExceptionHandler接口类);仅仅在这里做初始化的工作,可以让UncaughtExceptionHandler这个类从一开始就拦截app运行时候的任何uncaught异常。

写CrashHandler类可以用单例写,也可以直接new对象。其实个人觉得这里单例也没太必要,直接new一个对象,这个对象反正是要被set到Thread类里去的

/**
 * Sets the default uncaught exception handler. This handler is invoked in
 * case any Thread dies due to an unhandled exception.
 *
 * @param handler
 *            The handler to set or null.
 */
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
    Thread.defaultUncaughtHandler = handler;
}

上面的Thread.defaultUncaughtHandler就是系统的一个静态的实例。现在明白了其实在外面用static维持一个单例对象也没多大必要的原因了吧,系统里为我们维持了一个单例对象。

好了用单例初始化CrashHandler和直接new的两种方法都写出来吧

/** 保证只有一个CrashHandler实例 */
public CrashHandler() {

}

//CrashHandler实例
private static CrashHandler INSTANCE = new CrashHandler();
/** 获取CrashHandler实例 ,单例模式 */
public static CrashHandler getInstance() {
    return INSTANCE;
}

下面我们需要讨论的是如何实现拦截异常。其实在android系统中有异常就会报错,闪退,application not response ANR。我们要做的是要分担下系统为我们默认对异常处理的任务而已。系统默认处理异常不友好,半天不响应,返回键也没用,最后会弹出个窗来要用户自己决定是否结束或者等待。

看android里Thread的源代码可以看到,还有个

public class ThreadGroup implements Thread.UncaughtExceptionHandler{

实现了Thread里面UncaughtExceptionHandler的接口,这个类里有系统线程组。。。好了这系统代码也不太好看

//获取系统默认的UncaughtException处理器
    mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();

我们需要获取系统的默认的defaultUncaughtHandler。不过疑惑的是这个defaultUncaughtHandler是谁初始化或者设置过去的呢,在Thread类里没有初始化过程。应该是系统给初始化设置过去的,暂时没找到具体代码实现。。。有知道的教教我呗!

然后呢,我们需要把自己写的UncaughtException类设置到系统变量defaultUncaughtHandler中去。

//设置该CrashHandler为程序的默认处理器
    Thread.setDefaultUncaughtExceptionHandler(this);

Ok,这里其实有点乱了。理理,首先我们需要获取一个系统默认的异常处理对象defaultUncaughtHandler,然后我们要把系统默认的异常处理对象设置成我们自己写的一个实现UncaughtException接口的对象。为什么要这么做呢?

首先系统默认的异常处理对象defaultUncaughtHandler维持了一个系统的异常处理对象,获取到这个系统处理对象可以通过调用uncaughtException(thread, ex);方法实现系统默认的异常处理。

而我们自己要把自己写的一个实现UncaughtException接口的对象设置成系统默认的异常处理对象是为了让系统检测到未catch的异常是会调用我们自己实现的uncaughtException(thread, ex)方法。然后在此方法里面实现处理逻辑。

理理就知道了,我们需要在实现了UncaughtException接口的CrashHandler类的uncaughtException方法里面实现处理逻辑。并且完成了自己的处理逻辑之后要执行系统原理默认对未捕获异常的处理。

PS:为什么一定要自己处理完后让系统默认的异常处理对象再处理一遍呢,因为系统默认有对异常处理有默认的处理方案,比如runtimeexcption运行时异常,nullexception空指针异常怎么处理。其实都是卡死半天,然后弹窗让用户决定怎么处理,等待还是结束。这个弹窗的两个选项,用户选择了等待,说明这个卡死状态是正常了,也许是网络请求慢但是确实是逻辑没错导致的。用户选择了结束,说明了这个确实是异常要立马结束掉。选择结束也就是用户选择了系统默认的异常处理方式,即调用了类似的defaultUncaughtHandler.uncaughtException(thread, ex)方法处理。

/**
 * 当UncaughtException发生时会转入该函数来处理
 */
@Override
public void uncaughtException(Thread thread, Throwable ex) {
    if (!handleException(ex) && mDefaultHandler != null) {
        //如果用户没有处理则让系统默认的异常处理器来处理
            mDefaultHandler.uncaughtException(thread, ex);

//          mDefaultHandler.uncaughtException(null, ex);

//      android.os.Process.killProcess(android.os.Process.myPid());
//      System.exit(0);
    } else {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Log.e(TAG, "error : ", e);
        }
        //退出程序
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(0);
    }
}

这里回调接口方法的handleException(ex)是我们对异常的自定义处理。先不考虑。
其实这里模拟了系统的默认异常处理方式,即当没有默认异常处理的时候,会线程睡眠3秒然后退出应用。等价于卡死三秒然后弹窗提醒用户选择。
自己处理完对未捕获异常处理后交由系统默认异常处理对象处理,默认就退出应用了。
但是呢,这里我发现如果是mDefaultHandler.uncaughtException(thread, ex);处理的话第一次是正常退出应用的,当第二次点击应用图标的时候并没有执行到我们自己处理异常方法里。第三次点击应用图标时候直接弹出我们的异常处理方式。这是为什么呢?我的观点是系统有缓存异常机制,任务一样的错误一样的处理方式,就不再会执行我们自己写的异常处理方式里了,直接退出。这是我的观点。求反驳批判。
所以我让mDefaultHandler.uncaughtException(null, ex);中的一个参数都传null则系统不会缓存记录异常,因为是null的,没异常啊~我这算是在调戏系统么。所以每次都能正常执行我们的处理方法。
当然你也可以在系统处理异常方式里面写上我们自己的异常处理方式。但是如果最后不退出应用的话是会卡死的,,,,,,其实这里个人更推荐自己写android.os.Process.killProcess(android.os.Process.myPid());因为,如果传一个null给系统默认异常处理会报null指针异常的~~

我们再来聊聊自己怎么处理异常

/**
 * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
 * 
 * @param ex
 * @return true:如果处理了该异常信息;否则返回false.
 */
private boolean handleException(Throwable ex) {
    if (ex == null) {
        return false;
    }
    //使用Toast来显示异常信息
    new Thread() {
        @Override
        public void run() {
            Looper.prepare(); 
            Toast.makeText(mContext, "出错了~~~", Toast.LENGTH_LONG).show();
            TestService.getInstance().sendError("error:made by byl");
            Looper.loop(); 
        }
    }.start();

    //收集设备参数信息 
    collectDeviceInfo(mContext);
    //保存日志文件 
    saveCrashInfo2File(ex);

    return false;
}

//收集设备参数信息 collectDeviceInfo(mContext);
//保存日志文件 saveCrashInfo2File(ex);
这里有两个方法。保存出错日志。我觉得类似umeng这种错误分析的系统肯定也是这样做处理的,要不然可以知道什么手机出什么错误的统计一清二楚的。
这里关键在TestService这个后台服务。
顺便说下这里的Looper.prepare和Looper.loop方法.网上的说法Toast要想在子线中显示,就必须在当前线程中存在一个消息队列(Looper)。 具体Handler的操作,你看下Toast源码就知道了。好吧~

很讨厌面试的时候很多人问handler啊,Looper对象啊,message消息队列啊,然后我会很笼统的说android里面的Looper是一个轮询的类,handler每次send一个消息的时候会把消息放到消息队列里面去,然后looper去轮询这个消息队列去一个一个的处理消息。其实我能理解的也就是这一层,具体怎么轮询,怎么处理消息的其实代码具体怎么实现我也不知道啊~相信很多面试官也不清楚!

public class TestService extends Service 

TestService是一个服务了,在第一个Activity启动的时候就

Intent intent = new Intent(this, TestService.class);
    startService(intent);

这个后台服务默默地等待着系统出现未捕获的异常,然后执行自己的方法

    Intent intent = new Intent(this, SendErrorActivity.class);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.putExtra("msg", message);
    startActivity(intent);
    stopSelf();

然后这个SendErrorActivity呢可以实现弹窗提示的功能,具体逻辑要看业务需求。
再说下这个SendErrorActivity的具体实现吧
manifest里面配置

<activity
        android:name="com.example.testerror.SendErrorActivity"
        android:screenOrientation="portrait"
        android:theme="@style/bklistDialog" >
    </activity>

style里面配置bklistDialog

<style name="bklistDialog" parent="@android:style/Theme.Dialog">
    <item name="android:windowFrame">@android:color/transparent</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <!--控制灰度的值,当为1时,界面除了我们的dialog内容是高亮显示的,dialog以外的区域是黑色的,完全看不到其他内容,系统的默认值是0.5,而已根据自己的需要调整-->
    <item name="android:backgroundDimAmount">0.3</item>
    <item name="android:windowAnimationStyle">@null</item>
</style>

layout文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_marginLeft="25dp"
android:layout_marginRight="25dp" >

<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:background="#ffffff"
    android:gravity="center"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:padding="10dp"
            android:text="提示"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dp"
            android:text="是否发送错误信息?"
            android:textColor="#A19D94"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/info_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:textColor="#A19D94"
            android:textSize="16sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/ok"
            android:layout_width="fill_parent"
            android:layout_height="45dp"
            android:layout_weight="1"
            android:gravity="center"
            android:padding="10dp"
            android:text="确 定"
            android:textSize="18sp" />

        <Button
            android:id="@+id/cancel"
            android:layout_width="fill_parent"
            android:layout_height="45dp"
            android:layout_weight="1"
            android:gravity="center"
            android:padding="10dp"
            android:text="取消"
            android:textSize="18sp" />
    </LinearLayout>
</LinearLayout>

</RelativeLayout>

SendErrorActivity里面的oncreate方法具体实现

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_loginoutinfo);
    //全屏显示
    getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    ok=(Button) findViewById(R.id.ok);
    cancel=(Button) findViewById(R.id.cancel);
    error_msg=getIntent().getStringExtra("msg");
    ok.setOnClickListener(this);
    cancel.setOnClickListener(this);
}

OK,花了两个小时写下了这个东西研究了下还是很值得的。纯手敲啊,麻烦高人赐教啊!谢了

你可能感兴趣的:(android)