国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > 综合技术 > 手写CrashHandler实现UncaughtExceptionHandler拦截android异常

手写CrashHandler实现UncaughtExceptionHandler拦截android异常

来源:程序员人生   发布时间:2015-05-07 10:03:48 阅读次数:3791次

手写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这个类从1开始就拦截app运行时候的任何uncaught异常。

写CrashHandler类可以用单例写,也能够直接new对象。其实个人觉得这里单例也没太必要,直接new1个对象,这个对象反正是要被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就是系统的1个静态的实例。现在明白了其实在外面用static保持1个单例对象也没多大必要的缘由了吧,系统里为我们保持了1个单例对象。

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

/** 保证只有1个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,这里其实有点乱了。理理,首先我们需要获得1个系统默许的异常处理对象defaultUncaughtHandler,然后我们要把系统默许的异常处理对象设置成我们自己写的1个实现UncaughtException接口的对象。为何要这么做呢?

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

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

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

PS:为何1定要自己处理完后让系统默许的异常处理对象再处理1遍呢,由于系统默许有对异常处理有默许的处理方案,比如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秒然后退出利用。等价于卡死3秒然后弹窗提示用户选择。
自己处理完对未捕获异常处理后交由系统默许异常处理对象处理,默许就退出利用了。
但是呢,这里我发现如果是mDefaultHandler.uncaughtException(thread, ex);处理的话第1次是正常退出利用的,当第2次点击利用图标的时候并没有履行到我们自己处理异常方法里。第3次点击利用图标时候直接弹出我们的异常处理方式。这是为何呢?我的观点是系统有缓存异常机制,任务1样的毛病1样的处理方式,就不再会履行我们自己写的异常处理方式里了,直接退出。这是我的观点。求反驳批评。
所以我让mDefaultHandler.uncaughtException(null, ex);中的1个参数都传null则系统不会缓存记录异常,由于是null的,没异常啊~我这算是在调戏系统么。所以每次都能正常履行我们的处理方法。
固然你也能够在系统处理异常方式里面写上我们自己的异常处理方式。但是如果最后不退出利用的话是会卡死的,,,,,,其实这里个人更推荐自己写android.os.Process.killProcess(android.os.Process.myPid());由于,如果传1个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这类毛病分析的系统肯定也是这样做处理的,要不然可以知道甚么手机出甚么毛病的统计1清2楚的。
这里关键在TestService这个后台服务。
顺便说下这里的Looper.prepare和Looper.loop方法.网上的说法Toast要想在子线中显示,就必须在当前线程中存在1个消息队列(Looper)。 具体Handler的操作,你看下Toast源码就知道了。好吧~

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

public class TestService extends Service 

TestService是1个服务了,在第1个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⑻"?>
<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,花了两个小时写下了这个东西研究了下还是很值得的。纯手敲啊,麻烦高人赐教啊!谢了

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生