Crash: 处理UncaughtExcption,捕获未处理异常信息,界面友好提示用户

Crash: 处理UncaughtExcption,友好提示用户,捕获错误信息

相信大家在APP使用过程都遇到过,应用程序异常崩溃,屏幕一黑闪退,这种情况称之为Crash。出现的原因是由于程序运行过程中产生了未知异常UncaughtException,当程序发生Crash时,系统会杀死程序,出现闪退,这种情况的用户体验不好,而且开发人员也不能知道用户发生了何种异常。
那么问题来了,发生Crash时我们需要给用户提供一个友好的提醒,并且捕获用户的错误信息。
错误信息Activity

捕获用户Crash信息的两种思路

  • 1.集成第三方测试SDK,
    做的比较好的有 云测TestIn
    腾讯云测等等,这里只要看下文档基本10行以内代码就能搞定了,使用简单,但也有局限性。
  • 2.通过自定义实现,通过UncaughtExceptionHandler处理未捕获异常。(下面着重说说这种方式)
    具体思路:首先为程序指定UncaughtExceptionHandler处理程序,当未捕获异常发生时,会调用处理程序的uncaughtException方法,我们在该方法中获得相应异常信息,然后做出相应处理 弹出友好的提示框,保存异常信息(或上传到服务器)

UncaughtExceptionHandler文档是这么介绍的

java.lang
接口 Thread.UncaughtExceptionHandler
所有已知实现类:
ThreadGroup
正在封闭类:Thread
public static interface Thread.UncaughtExceptionHandler
当 Thread 因未捕获的异常而突然终止时,调用处理程序的接口。
当某一线程因未捕获的异常而即将终止时,Java 虚拟机将使用 Thread.getUncaughtExceptionHandler() 查询该线程以获得其 UncaughtExceptionHandler 的线程,并调用处理程序的 uncaughtException 方法,将线程和异常作为参数传递。如果某一线程没有明确设置其 UncaughtExceptionHandler,则将它的 ThreadGroup 对象作为其 UncaughtExceptionHandler。如果 ThreadGroup 对象对处理异常没有什么特殊要求,那么它可以将调用转发给默认的未捕获异常处理程序。

  • 通过指定一个Thread.setDefaultUncaughtExceptionHandler()方法指定未知异常处理程序
//设置该线程由于未捕获到异常而突然终止时调用的处理程序
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {
        //处理异常,我们还可以把异常信息写入文件,以供后来分析。
        String stackTraceString = getThrowableStraceStr(thread, throwable);
        Log.e(TAG,stackTraceString);

        startErroActivity(stackTraceString);
        killCurrentProcess();
    }
});
  • 下面 说一下具体的几个方法
    获取异常及异常的追踪信息,异常信息和追踪信息如下图
    异常信息和追踪信息的区别
/**
 * 获取此 throwable 及其追踪信息
 * @param thread
 * @param throwable
 * @return
 */
private static String getThrowableStraceStr(Thread thread, Throwable throwable) {
    Log.e("CrashException", thread.getName() + "--Exception:" + throwable + "\n\n");
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    throwable.printStackTrace(pw);//
    return sw.toString();
}
  • 当捕获到异常追踪信息后,我们的处理方式时打开一个专门用于的错误信息Activity用于提示用户,在该并可以用户选择是否重新启动应用程序。
    这里有三个地方需要注意的是:
    • 1.将错误信息通过Intent.putExtra(erroInfo)传递给错误提示页面需要保证文本长度不超过128K
    • 2.打开新的ErroActivity是在一个单独的进程中,所以调用杀死当前进程不会杀掉ErroActivity所在进程
    • 3.设置ErroActivity的意图,清空回退栈中所有activity intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK)
private static void startErroActivity(String straceMsg) {
    //Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent.
    //The limit is 1MB on Android but some devices seem to have it lower.
    //See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html
    //And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171
    if (straceMsg.length() > MAX_STACK_TRACE_SIZE) {
        String disclaimer = " [stack trace too large]";
        straceMsg = straceMsg.substring(0, MAX_STACK_TRACE_SIZE - disclaimer.length()) + disclaimer;
    }

    //启动错误页面
    Intent intent=new Intent(application,ErroActivity.class);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
    intent.putExtra(EXTRA_STRACE_MESSAGE,straceMsg);
    application.startActivity(intent);
}
  • 在开启错误信息页面后,结束当前进程
/**
 * 结束进程
 * INTERNAL method that kills the current process. It is used after
 * restarting or killing the app.
 */
private static void killCurrentProcess() {
    //杀死当前进程
    android.os.Process.killProcess(android.os.Process.myPid());
    System.exit(10);
}

捕获异常完整代码

package com.crash.k.crashuncaughtexception.utils;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Looper;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.widget.Toast;

import com.crash.k.crashuncaughtexception.ErroActivity;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * Created by K on 2015/12/21.
 */
public class CrashUncaugtExceptionUtils {

    public final static String EXTRA_STRACE_MESSAGE="extra_strace_message";
    private static final String TAG="CrashException";

    /**设置传递错误堆栈信息最大长度 保证不超过128 KB*/
    private static final int MAX_STACK_TRACE_SIZE = 131071; //128 KB - 1

    private static Context mContext;
    private static Application application;

     /**
     * 初始化
     */
    public static void install(Context context){

//设置该线程由于未捕获到异常而突然终止时调用的处理程序
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {
        //处理异常,我们还可以把异常信息写入文件,以供后来分析。
        String stackTraceString = getThrowableStraceStr(thread, throwable);
        Log.e(TAG,stackTraceString);

        startErroActivity(stackTraceString);
        killCurrentProcess();
    }
});

        mContext=context;
        application=(Application)context.getApplicationContext();
        application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle bundle) {
                Log.i(TAG, "onActivityCreated");

            }

            @Override
            public void onActivityStarted(Activity activity) {

            }

            @Override
            public void onActivityResumed(Activity activity) {
                Thread.UncaughtExceptionHandler unCaughtExceptionHandler= Thread.getDefaultUncaughtExceptionHandler();
                Log.i("LifecycleCallbacks",activity.getClass().getName()+"-->\n"+ unCaughtExceptionHandler.getClass().getName() + "");
            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {

            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {

            }
        });
    }

    private static void startErroActivity(String straceMsg) {
        //Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent.
        //The limit is 1MB on Android but some devices seem to have it lower.
        //See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html
        //And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171
        if (straceMsg.length() > MAX_STACK_TRACE_SIZE) {
            String disclaimer = " [stack trace too large]";
            straceMsg = straceMsg.substring(0, MAX_STACK_TRACE_SIZE - disclaimer.length()) + disclaimer;
        }

        //启动错误页面
        Intent intent=new Intent(application,ErroActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
        intent.putExtra(EXTRA_STRACE_MESSAGE,straceMsg);
        application.startActivity(intent);
    }

    /**
     * 获取此 throwable 及其追踪信息
     * @param thread
     * @param throwable
     * @return
     */
    private static String getThrowableStraceStr(Thread thread, Throwable throwable) {
        Log.e("CrashException", thread.getName() + "--Exception:" + throwable + "\n\n");
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        throwable.printStackTrace(pw);//
        return sw.toString();
    }

    /**
     * 获取应用的启动页Activity
     * INTERNAL method used to get the default launcher activity for the app.
     * If there is no launchable activity, this returns null.
     *
     * @param context A valid context. Must not be null.
     * @return A valid activity class, or null if no suitable one is found
     */
    @SuppressWarnings("unchecked")
    public static Class<? extends Activity> getLauncherActivity(Context context) {
        Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
        if (intent != null) {
            try {
                return (Class<? extends Activity>) Class.forName(intent.getComponent().getClassName());
            } catch (ClassNotFoundException e) {
                //Should not happen, print it to the log!
                Log.e(TAG, "Failed when resolving the restart activity class via getLaunchIntentForPackage, stack trace follows!", e);
            }
        }
        return null;
    }



    /**
     * 结束进程
     * INTERNAL method that kills the current process. It is used after
     * restarting or killing the app.
     */
    private static void killCurrentProcess() {
        //杀死当前进程
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(10);
    }
}

在Application中完成初始化

封装好了异常处理,在Application中一行代码即可完成初始化了

public class CrashApp extends Application{
    @Override
    public void onCreate() {
        super.onCreate();

        //初始化
        CrashUncaugtExceptionUtils.install(this);
    }
}

ErroActivity中的异常处理

  • 1.重新启动APP,首先找到需要LauncherActivity
public static Class<? extends Activity> getLauncherActivity(Context context) {
    Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
    if (intent != null) {
        try {
            return (Class<? extends Activity>) Class.forName(intent.getComponent().getClassName());
        } catch (ClassNotFoundException e) {
            //Should not happen, print it to the log!
            Log.e(TAG, "Failed when resolving the restart activity class via getLaunchIntentForPackage, stack trace follows!", e);
        }
    }
    return null;
}
  • 2 .显示异常信息,具体处理可以保存到本地文件统一上传,也可以每次发生异常则上传,具体操作酌情处理
    以下错误信息的代码
public class ErroActivity extends AppCompatActivity {

    private Context mContext;
    private String mErroDetailsStr;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_erro);
        initData();
        initViews();
    }

    private void initData(){
        mContext=this;
        String erroInfo= getIntent().getStringExtra(CrashUncaugtExceptionUtils.EXTRA_STRACE_MESSAGE);
        mErroDetailsStr=getErroDetailsInfo()+"\n strace \n"+erroInfo;
    }

    private void initViews()
    {
        Button btnRestart=(Button)findViewById(R.id.btn_restart);
        btnRestart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Class launcherActivity= CrashUncaugtExceptionUtils.getLauncherActivity(mContext);

                if(launcherActivity!=null)
                {
                    Intent intent=new Intent(mContext,launcherActivity);
                    startActivity(intent);
                    finish();
                }else{
                    Toast.makeText(mContext,"重启失败了",Toast.LENGTH_SHORT).show();
                }
            }
        });

        TextView tvInfo=(TextView)findViewById(R.id.tv_erro_info);
        tvInfo.setText(mErroDetailsStr);
    }

    /**获取错误信息
     *
     * @param thread
     * @param throwable
     * @return
     */
    private String getErroDetailsInfo()
    {
        String erroDetailInfo="";
        //设备名称
        erroDetailInfo+="DeviceName:"+ AppInfoUtils.getDeviceModelName()+ " \n";
        //版本号
        erroDetailInfo+="VersionName:"+AppInfoUtils.getVersionName(mContext)+ " \n";

        DateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
        //记录当前出错时间
        erroDetailInfo+="currentTime"+dateFormat.format(new Date())+ " \n";

        //安装时间
        erroDetailInfo+="BuildTime:"+AppInfoUtils.getBuildDateAsString(mContext,dateFormat)+ " \n";

        return  erroDetailInfo;
    }
}

获取设备号,版本号,安装时间的几个方法

package com.crash.k.crashuncaughtexception.utils;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Build;

import java.text.DateFormat;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Created by K on 2015/12/27.
 */
public class AppInfoUtils {

    /**
     * 获取应用版本名称.
     *
     */
    public static String getVersionName(Context context) {
        try {
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionName;
        } catch (Exception e) {
            return "Unknown";
        }
    }

    /**
     * 获取设备名称
     */
    public static String getDeviceModelName() {
        String manufacturer = Build.MANUFACTURER;
        String model = Build.MODEL;
        if (model.startsWith(manufacturer)) {
            return capitalize(model);
        } else {
            return capitalize(manufacturer) + " " + model;
        }
    }

    /**
     * 获取应用安装时间
     * @param context 当前上下文,必填
     * @param dateFormat 时间格式化
     * @return
     */
    public static String getBuildDateAsString(Context context, DateFormat dateFormat) {
        String buildDate;
        try {
            ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
            ZipFile zf = new ZipFile(ai.sourceDir);
            ZipEntry ze = zf.getEntry("classes.dex");
            long time = ze.getTime();
            buildDate = dateFormat.format(new Date(time));
            zf.close();
        } catch (Exception e) {
            buildDate = "Unknown";
        }
        return buildDate;
    }


    /***
     * 如果字符串为null则默认转为 “”
     * @param s
     * @return
     */
    private static String capitalize(String s) {
        if (s == null || s.length() == 0) {
            return "";
        }
        return  s;
    }

}

源码下载地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值