在《程序的需求层次》这篇文章,我们可以看到程序可用性的重要性。当然,不读这篇文章,我们也知道,它很重要。但是,可能没觉得那么重要。在我写过的所有程序里面,就有这么一个程序,由于前期不怎么重视异常处理,结果后期经常崩溃,导致公司所有人都对它不放心,即使后来已经改善了很多,也总会把它作为一个事提起(作为开发的我们,心里会很不爽的啊!)。更严重的是,程序崩溃可能丢失用户数据,对用户来说可能是灾难性的。我也经常在下载App的时候,看用户评论,里面有各种对闪退的恐惧、唾弃以及无奈。所以在这篇文章,我们主要看看在android 开发中,怎么避免程序崩溃。
在进入具体的讨论之前,我想说说提高可用性的基本策略。我觉得可以从三个方面入手:
- 减错
- 容错
- 纠错
减错就是减少错误,这部分应该是大家做得最多的,我们一般都有专门的测试部门,来帮忙发现错误,然后更改,目的就是减少产品正式面市后的错误。容错,就是在错误发生后,尽可能的让程序其他部分正常运行的策略。今天,我们说的避免程序crash就是属于这种类型。它主要有三个子策略:限定范围、回滚、使用备份。最后一个是纠错,这个大家用的就更少了,就是程序发现错误,然后自动把错误解决的策略。也包括三个主要的子策略:重试、检查依赖、重置。废话就这么多了,更具体的以后有时间再说。今天我们主要是使用限定范围策略来和闪退say byebye啦。
首先,我们来看看哪些异常会导致App崩溃。我们知道java异常有两种,一种是Error,一种是Exception。Error是我们自己处理不来的,一般是虚拟机错误或者内存错误。还好,这种错误发生的概率不是很大。更多是Exception, 并且是运行时异常,导致的程序挂掉(还有一种Exception是非运行时异常,这个一般我们都会被强制处理,不然编译器会报错,所以不会导致程序挂掉)。下面的程序就是模拟程序崩溃的App。
它有4个按钮,前三个按钮点击后,会在UI线程、异步任务、自定义线程里向外抛数组越界异常(一种运行时异常)。
public void onExceptionBt(View view)
{
int[] array = new int[]{1,2,3};
int a1 = array[5]; //这里会抛出异常
}
结果都导致了程序崩溃。
第四个按钮对应的是用try – catch语句捕获UI线程里的异常,限制它的扩散。
public void onExceptionInCatchBt(View view) { try { onExceptionBt(view); } catch(Exception e) { ExceptionUtil.log(e, this); } }
结果,第四个按钮点击后,程序没有崩溃,而是我们打印的出错信息。
所以,我们的主要策略之一就是利用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线程还是活的,所以还有继续工作的可能。如果这些其它线程也很重要,不能随便终止,那么还是乖乖采用第一种方法吧。