在《程序的需求层次》这篇文章,我们可以看到程序可用性的重要性。当然,不读这篇文章,我们也知道,它很重要。但是,可能没觉得那么重要。在我写过的所有程序里面,就有这么一个程序,由于前期不怎么重视异常处理,结果后期经常崩溃,导致公司所有人都对它不放心,即使后来已经改善了很多,也总会把它作为一个事提起(作为开发的我们,心里会很不爽的啊!)。更严重的是,程序崩溃可能丢失用户数据,对用户来说可能是灾难性的。我也经常在下载App的时候,看用户评论,里面有各种对闪退的恐惧、唾弃以及无奈。所以在这篇文章,我们主要看看在android 开发中,怎么避免程序崩溃。

在进入具体的讨论之前,我想说说提高可用性的基本策略。我觉得可以从三个方面入手:

减错

容错

纠错

减错就是减少错误,这部分应该是大家做得最多的,我们一般都有专门的测试部门,来帮忙发现错误,然后更改,目的就是减少产品正式面市后的错误。容错,就是在错误发生后,尽可能的让程序其他部分正常运行的策略。今天,我们说的避免程序crash就是属于这种类型。它主要有三个子策略:限定范围、回滚、使用备份。最后一个是纠错,这个大家用的就更少了,就是程序发现错误,然后自动把错误解决的策略。也包括三个主要的子策略:重试、检查依赖、重置。废话就这么多了,更具体的以后有时间再说。今天我们主要是使用限定范围策略来和闪退say byebye啦。

首先,我们来看看哪些异常会导致App崩溃。我们知道java异常有两种,一种是Error,一种是Exception。Error是我们自己处理不来的,一般是虚拟机错误或者内存错误。还好,这种错误发生的概率不是很大。更多是Exception, 并且是运行时异常,导致的程序挂掉(还有一种Exception是非运行时异常,这个一般我们都会被强制处理,不然编译器会报错,所以不会导致程序挂掉)。下面的程序就是模拟程序崩溃的App。

ios智联登录时提示什么广告标识 打开智联闪退怎么回事_java

它有4个按钮,前三个按钮点击后,会在UI线程、异步任务、自定义线程里向外抛数组越界异常(一种运行时异常)。

 

public void onExceptionBt(View view)
{
  int[] array = new int[]{1,2,3};
  int a1 = array[5]; //这里会抛出异常
}

 

结果都导致了程序崩溃。

ios智联登录时提示什么广告标识 打开智联闪退怎么回事_java_02

第四个按钮对应的是用try – catch语句捕获UI线程里的异常,限制它的扩散。

public void onExceptionInCatchBt(View view)
{
    try
    {
        onExceptionBt(view);
    }
    catch(Exception e)
    { 
        ExceptionUtil.log(e, this);
    }
}

 

结果,第四个按钮点击后,程序没有崩溃,而是我们打印的出错信息。

ios智联登录时提示什么广告标识 打开智联闪退怎么回事_java_03

所以,我们的主要策略之一就是利用try – catch,限定异常范围。

有人就说了,在UI线程的每个入口方法里写上try-catch,是不是有点苦逼啊。的确是,为了简化,我的建议是自定义一个Handler,重写它的dispatchMessage方法,把try-catch放在里面。

package com.lanbeetou.android.avaiable;

import android.content.Context;
import android.os.Handler;
import android.os.Message;

public class ExceptionHandler extends Handler {
    
    private static ExceptionHandler handler;
    
    private Context context;
    private ExceptionHandler(Context context)
    {
        super();
        this.context = context;
    }
    
    public static ExceptionHandler getExceptionHandler(Context context)
    {
        if(handler == null)
        {
            handler = new ExceptionHandler(context);
        }
        return handler;
    }
    
    @Override
    public void dispatchMessage(Message msg) {
       try
       {
           super.dispatchMessage(msg);
       }
       catch(Exception e)
       {
           ExceptionUtil.log(e, context);
       }
    }
}

这样,入口方法的调用都转发给自定义的Handler,由Handler统一处理异常。

public void onExceptionBt(View view)
{
     ExceptionHandler.getExceptionHandler(this).post(new Runnable() {
            @Override
            public void run() {
                int[] array = new int[]{1,2,3};
                int a1 = array[5];
            }
      });
 }

还嫌麻烦?有更好的办法?大家凑和着用吧。

好消息是异步操作或者自定义线程的异常捕获,不用这么苦逼,可以通过Thread.setUncaughtExceptionHandler方法来设置默认的未捕获异常处理方法。

package com.lanbeetou.android.avaiable;

import java.lang.Thread.UncaughtExceptionHandler;

import android.content.Context;

public class UncaughtHandler implements UncaughtExceptionHandler {

    private Context context;

    public void init(Context context) {

        this.context = context;
        Thread.setDefaultUncaughtExceptionHandler(this);

    }

    @Override
    public void uncaughtException(Thread arg0, Throwable e) {
        ExceptionUtil.log(e, context);
    }

}

 

设置成功后,比如说此时发生了一个异常,并且没有捕获,则该异常所在的线程会被终止,同时该异常处理方法会被调用。所以对于UI线程来说,这个方法不适用,不然UI线程被终止,和崩溃无异。而对应其他线程,虽然被停止,但是Ui线程还是活的,所以还有继续工作的可能。如果这些其它线程也很重要,不能随便终止,那么还是乖乖采用第一种方法吧。