请选择 进入手机版 | 继续访问电脑版

北南南北论坛

 找回密码
 立即注册
查看: 16|回复: 2

Android中的异常事件处理器

[复制链接]

563

主题

950

帖子

2694

积分

金牌会员

Rank: 6Rank: 6

积分
2694
发表于 2017-12-20 08:26:39 | 显示全部楼层 |阅读模式
Android应用不可避免的发生异常,即系统奔溃,这不是任何人能避免的,无论你的程序写的多么完美,它也不会完全避免异常的发生,其中的原因我们就不去研究了。当异常发生后,系统就会kill掉正在运行的程序,现象就是发生闪退或者提示用户程序无响应和停止运行,这是我们长达程序猿同胞们最不愿意见到的场景。更糟糕的是,当你上线后的应用发生异常,开发者确无法得知程序为何奔溃了,更谈不上去处理它了。幸好,Android提供了处理这类问题的方法,即是Thread类中的一个方法setDefaultUncaughtExceptionHandler。

  1. public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
  2.         defaultUncaughtExceptionHandler = eh;
  3.     }
复制代码


实现方法:让我们的类继承Thread.UncaughtExceptionHandler,然后重写它里面的uncaughtException方法就可以了
当异常发生的时候,系统就会回调UncaughtExceptionHandler的uncaughtException的方法,在uncaughtException方法中我们就可以获取到异常信息,我们可以选择把异常信息保存在SD卡中,然后在合适的时机上传到网络服务器中,这样开发者就可以分析用户的错误然后在后面的版本中去修复这个bug,当然,我们为了给用户一个良好的体验,可以选择弹出一个对话框甚至跳转一个界面告诉用户程序奔溃了,然后在退出,这样做比闪退的体验好了不止一倍了。
下面就是一个异常处理器的实现类:

  1. public class CrashHandler implements Thread.UncaughtExceptionHandler {

  2.    private static final String TAG = "CrashHandler";
  3.    private static final boolean DEBUG = true;
  4.    private static final String PATH = Environment.getExternalStorageDirectory()
  5.            .getAbsolutePath()
  6.            + File.separator
  7.            + File.separator;

  8.    private static final String FILE_NAME = "crashText";
  9.    private static final String FILE_NAME_SUFFIX = ".log";


  10.    private static CrashHandler sInstance = new CrashHandler();
  11.    private Thread.UncaughtExceptionHandler mDefaultCrasHandler;

  12.    private Context mContext;

  13.    public CrashHandler() {

  14.    }

  15.    public static CrashHandler getsInstance() {
  16.        return sInstance;
  17.    }

  18.    public void init(Context context) {
  19.        mDefaultCrasHandler = Thread.getDefaultUncaughtExceptionHandler();
  20.        Thread.setDefaultUncaughtExceptionHandler(this);
  21.        mContext = context.getApplicationContext();
  22.    }

  23.    /**
  24.     * 这是最关键的函数,当程序中有未捕获的异常,系统将会自动调用这个方法,
  25.     * thread为出现未捕获异常的线程,ex为为未捕获的异常,有了这个ex,我们就可以得到
  26.     * 异常信息了
  27.     *
  28.     * @param thread
  29.     * @param ex
  30.     */
  31.    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
  32.    @Override
  33.    public void uncaughtException(Thread thread, Throwable ex) {

  34.        //导出异常信息到SD卡中
  35.        try {
  36.            dumpExceptionToSDCard(ex);
  37.            //上传异常信息到服务器,便于开发人员分析日志从而解决bug
  38.            uploadExceptionToServer();
  39.        } catch (Exception e1) {
  40.            e1.printStackTrace();
  41.        }
  42.            ex.printStackTrace();
  43.        //如果系统提供了默认的异常处理器,则交给系统去结束程序,否则就由自己结束自己
  44.        if (mDefaultCrasHandler != null) {
  45.            mDefaultCrasHandler.uncaughtException(thread, ex);
  46.        } else {
  47.            Process.killProcess(Process.myPid());
  48.        }

  49.    }

  50.    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
  51.    private void dumpExceptionToSDCard(Throwable ex) throws IOException {
  52.        //如果SD卡不存在或无法使用,则无法把异常信息写入SD卡中
  53.        if (!Environment.getExternalStorageState()
  54.                .equals(Environment.MEDIA_MOUNTED)) {
  55.            if (DEBUG) {
  56.                Log.i(TAG, "dumpExceptionToSDCard: SD卡不存在");
  57.                return;
  58.            }
  59.        }
  60.        File dir = new File(PATH);
  61.        if (!dir.exists()) {
  62.            dir.mkdirs();
  63.        }
  64.        long current = System.currentTimeMillis();
  65.        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
  66.                .format(new Date(current));
  67.        File file = new File(PATH + FILE_NAME);
  68.        //先删除之前的异常信息
  69.        if (file.exists()) {
  70.            DeleteFile(file);
  71.        }
  72.        if (!file.exists()) {
  73.            file.mkdirs();
  74.        }
  75.        StackTraceElement[] stackTrace = ex.getStackTrace();

  76.        String error_text = "错误:" + ex.toString() + "  \n  ";
  77.        for (int i = 0; i < stackTrace.length; i++) {
  78.            error_text += stackTrace[i].getFileName() + " class:"
  79.                    + stackTrace[i].getClassName() + " method:"
  80.                    + stackTrace[i].getMethodName() + " line:"
  81.                    + stackTrace[i].getLineNumber() + "  \n  ";
  82.        }
  83.        try {
  84.            PrintWriter pw = new PrintWriter(
  85.                    new BufferedWriter(
  86.                            new FileWriter(file + File.separator
  87.                                    + time + FILE_NAME_SUFFIX)));
  88.            pw.println(time);
  89.            dumpPhoneInfo(pw);
  90.            pw.println();
  91.            ex.printStackTrace(pw);
  92.            pw.close();
  93.        } catch (Exception e) {
  94.            e.printStackTrace();
  95.            Log.i(TAG, "dumpExceptionToSDCard: " + e);
  96.        }
  97.    }

  98.    /**
  99.     * 写入手机的基本信息
  100.     *
  101.     * @param pw
  102.     */
  103.    private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {
  104.        PackageManager pm = mContext.getPackageManager();
  105.        PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
  106.        pw.print("App Version: ");
  107.        pw.print(pi.versionName);
  108.        pw.print('_');
  109.        pw.println(pi.versionCode);

  110.        //Android版本号
  111.        pw.print("OS Version: ");
  112.        pw.print(Build.VERSION.RELEASE);
  113.        pw.print('_');
  114.        pw.println(Build.VERSION.SDK_INT);

  115.        //手机制造商
  116.        pw.print("Vendor: ");
  117.        pw.println(Build.MANUFACTURER);

  118.        //手机型号
  119.        pw.print("Model: ");
  120.        pw.println(Build.MODEL);

  121.        //CUP架构
  122.        pw.print("CUP ABI: ");
  123.        pw.println(Build.CPU_ABI);

  124.        //奔溃发生时间
  125.        pw.print("CURRENT DATE: ");
  126.        Date currentDate = new Date();
  127.        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
  128.        pw.println(dateFormat.format(currentDate));
  129.    }

  130.    private void uploadExceptionToServer() {
  131.        //TODO Upload Exception Message To Your Web Server
  132.    }
复制代码


当应用奔溃的时候,CrashHandler会将异常信息及设备信息保存到SD卡中,接着将异常交给系统去处理,系统会帮助我们中止程序。
那么该如何去使用CrashHandler呢?很简单,我们在Application初始化的时候设置CrashHandler,如下所示:

  1. public class MyApplication extends Application {

  2.    @Override
  3.    public void onCreate() {
  4.        super.onCreate();
  5.        CrashHandler crashHandler = CrashHandler.getsInstance();
  6.        crashHandler.init(this);
  7.    }
  8. }
复制代码
回复

使用道具 举报

563

主题

950

帖子

2694

积分

金牌会员

Rank: 6Rank: 6

积分
2694
 楼主| 发表于 2017-12-20 08:28:45 | 显示全部楼层
这样我们就可以很方便的到服务器上去查看用户的奔溃信息了,需要注意的是,代码中被catch的异常是不会交给CrashHandler处理的,CrashHandler只能收到那些没有被捕获的异常,下面我们测试一下,如下所示,我们抛出一个异常。

  1. public class MainActivityextends AppCompatActivity{

  2.    @Override
  3.    protected void onCreate(Bundle savedInstanceState) {
  4.        super.onCreate(savedInstanceState);
  5.        setContentView(R.layout.activity_main);
  6.        int s = 2 / 0;//除数不能为0
  7.    }
  8. }
复制代码


在这里,我们为了不为用户增加内存的负担,当发生奔溃的时候,将以前保存在SD卡中的异常信息删除,这样就不会每次出现奔溃信息后就创建一个文件,节省内存。

  1. /**
  2.     * 递归删除文件和文件夹
  3.     * @param file    要删除的根目录
  4.     */
  5.    public void DeleteFile(File file){
  6.        if(file.isFile()){
  7.            file.delete();
  8.            return;
  9.        }
  10.        if(file.isDirectory()){
  11.            File[] childFile = file.listFiles();
  12.            if(childFile == null || childFile.length == 0){
  13.                file.delete();
  14.                return;
  15.            }
  16.            for(File f : childFile){
  17.                DeleteFile(f);
  18.            }
  19.            file.delete();
  20.        }
  21.    }
复制代码


在文章的开头我提到过,为了给用户好的体验,我决定跳转一个界面来提示用户发生了异常,如图所示这样的界面,用户的体验是不是会上一层楼呢?
我们在uncaughtException方法中调用跳转界面

  1. new Thread() {
  2.            @Override
  3.            public void run() {

  4.                Looper.prepare();
  5.                try {
  6.                    Intent intent = new Intent();
  7.                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
  8.                    intent.setAction("com.error.text.NOTIFY_ERROR");
  9.                    mContext.startActivity(intent);
  10.                } catch (Exception e) {
  11.                    e.printStackTrace();
  12.                }
  13.                Looper.loop();
  14.            }
  15.        }.start();
复制代码


然后在AndroidManifest.xml中配置Activity,记住这里是隐式启动Activity,代码如下所示:

  1. <activity
  2.            android:name=".ErrorActivity"
  3.            android:configChanges="orientation|keyboardHidden"
  4.            android:screenOrientation="portrait"
  5.            >
  6.            <intent-filter>
  7.                <action android:name="com.error.text.NOTIFY_ERROR" />

  8.                <category android:name="android.intent.category.DEFAULT" />
  9.            </intent-filter>
  10.     </activity>
复制代码


界面大致是这样的:

1.png
回复 支持 反对

使用道具 举报

563

主题

950

帖子

2694

积分

金牌会员

Rank: 6Rank: 6

积分
2694
 楼主| 发表于 2017-12-20 08:30:17 | 显示全部楼层
完全退出帮助类

  1. public class CompleteQuit extends Application {

  2.    private Stack<Activity> activityStack;
  3.    private static CompleteQuit instance;

  4.    private CompleteQuit() {
  5.    }

  6.    /**
  7.     * 单例模式中获取唯一的CompleteQuit实例
  8.     */
  9.    public static CompleteQuit getInstance() {
  10.        if (null == instance) {
  11.            instance = new CompleteQuit();
  12.        }
  13.        return instance;

  14.    }

  15.    /**
  16.     * 添加Activity到容器中
  17.     */

  18.    public void pushActivity(Activity activity) {
  19.        if (activityStack == null) {
  20.            activityStack = new Stack<Activity>();
  21.        }
  22.        activityStack.add(activity);
  23.    }

  24.    /**
  25.     * 退出栈中所有Activity(唯一列外)
  26.     */

  27.    @SuppressWarnings("rawtypes")
  28.    public void exitAllButOne(Class cls) {
  29.        while (true) {
  30.            Activity activity = currentActivity();
  31.            if (activity == null) {
  32.                break;
  33.            }
  34.            if (activity.getClass().equals(cls)) {
  35.                break;
  36.            }
  37.            popActivity(activity);
  38.        }
  39.    }

  40.    /**
  41.     * 退出栈中所有Activity
  42.     */
  43.    public void exitAll(boolean exit) {
  44.        while (true) {
  45.            Activity activity = currentActivity();
  46.            if (activity == null) {
  47.                break;
  48.            }
  49.            popActivity(activity);
  50.        }
  51.        if (exit) {
  52.            System.exit(0);
  53.        }
  54.    }

  55.    /**
  56.     * 获得当前栈顶Activity
  57.     */
  58.    public Activity currentActivity() {
  59.        Activity activity = null;
  60.        if (activityStack != null && !activityStack.empty())
  61.            activity = activityStack.lastElement();
  62.        return activity;
  63.    }

  64.    /**
  65.     * 退出栈顶Activity
  66.     */
  67.    public void popActivity(Activity activity) {
  68.        if (activity != null) {
  69.            // 在从自定义集合中取出当前Activity时,也进行了Activity的关闭操作
  70.            activity.finish();
  71.            activityStack.remove(activity);
  72.            activity = null;
  73.        }
  74.    }
  75. }
复制代码


到此我们的异常事件处理器就完成了,快快为你的应用加上默认的异常事件处理器吧!!!
2.png
3.png


回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


手机版|北南南北论坛  

GMT+8, 2018-1-24 07:52 , Processed in 0.075882 second(s), 33 queries .

© 2001-2016 VxWorks6 Inc.

快速回复 返回顶部 返回列表