做任何软件,都需要考虑异常情况的处理,这是软件的可维护性的一部分。
异常崩溃是一种罕见的极端异常情况,这种情况下,针对终端用户的UI反馈、事故设备的信息采集、向后台维护人员的数据反馈等,都需要精心的设计。
UI反馈
- 要做反馈,首先要抓到所有的异常崩溃。
异常崩溃都是App进程的异常,每个App进程都运行在该App的Application中,所以我们可以在Application上集中抓到所有的进程异常:
private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
private Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() {
if (uncaughtExceptionHandler == null) {
uncaughtExceptionHandler = CrashHandler.getInstance(this,this);
}
return uncaughtExceptionHandler;
}
private void init(){
Thread.setDefaultUncaughtExceptionHandler(getUncaughtExceptionHandler());
}
我们抓到所有的进程异常,然后统一抛给一个CrashHandler类去处理,这个CrashHandler要实现UncaughtExceptionHandler接口:
public class CrashHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread thread, Throwable ex) {
}
}
- 尽力保持用户数据的完整性,并设法恢复崩溃前的界面。
如果要在崩溃时重启App,就需要在退出App前,再次StartActivity
Intent i = mContext.getPackageManager().getLaunchIntentForPackage(mContext.getPackageName());
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
mContext.startActivity(i);
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
其中,android.os.Process.killProcess(Dalvik虚拟机方法)和System.exit(0)(常规java方法)起到的作用一样。
这里有一个关于Activity栈的陷阱,上面的代码可以重启入口Activity,但是,如果这时你这个App的Activity栈里还有其他的Activity,这些Activity是仍然存在的,不会被销毁。
推荐的做法是在Application中维护一个Activity的列表,专门管理所有的Activity,在必要时,通过这个列表,去销毁所有的Activity
private ActivityStack stack;//扩展LinkList自定义一个activity列表
//用set注入
@Override
public void initActivityStack(ActivityStack stack) {
this.stack=stack;
}
//activity在oncreate时做添加//
@Override
public void addActivity(Activity activity) {
if(stack!=null)stack.addStack(activity);
}
//activity在ondestroy时做删减//
@Override
public void removeActivity(Activity activity) {
if(stack!=null)stack.remove(activity);
}
@Override
public void clearAll() {
if(stack!=null)stack.clearAll();
}
在这个自定义的activity列表里,通过调用Activity的finish,来销毁Activity
public void clearAll() {
if(stack!=null&&stack.size()>0) {
for (Activity activity : stack) {
if (activity != null) {
activity.finish();
}
}
stack.clear();
}
}
注意,为了避免Application持有Activity导致内存泄露,在Activity的生命周期里不能只写入列,还要记得写出列。
- 然后要提示用户发生了一些事情,这里要谨慎措辞,最好根据产品特性设计一些符合产品气质的提示语,这里就不展开了。
- 最后,在自动重启和退出App之间寻找平衡,比如第一次崩溃当然可以自动重启,如果遇到特殊因素导致连续崩溃(如:接口问题或运行环境问题),就需要人为限制重启的次数或频率(比如,记录上次自动重启的时间,判断两次重启的时间间隔是否过窄),避免成为用户眼中的流氓软件。
信息采集
对异常崩溃了解的越多,就越容易处理它,所以我们要尽可能地采集相关信息。
对研发来说,最有用的当然是异常代码行(如MainActivity第61行)和异常原因(如空指针异常),在研发环境里,我们可以通过logcat读到这些信息,那没什么可说的,我们要考虑的是,如果异常崩溃发生在万里之外的生产环境,我们要怎样采集信息。
- 首先,要创建和保存本地log文件夹,专门保存这些信息,一方面,网络不是一直可靠的,保存到本地可以避免数据丢失;另一方面,无论是远程数据上传还是现场同僚手动拷贝,都需要有这样一个文件夹。
- 然后,要抓取异常代码行和异常原因,也就是你在logcat里读到的那些异常堆栈信息,所有的Java异常都会抛出一个Throwable,这里面就能找到这些异常堆栈信息(需要用printwriter去读)
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
- 最后,有些崩溃是在特定的软硬件环境下出现的,我们需要知道这些环境信息:
Map<String, String> infos = new HashMap<String, String>();
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
infos.put(field.getName(), field.get(null).toString());
} catch (Exception e) {
Log.e(TAG, "an error occured when collect crash info", e);
}
}
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
数据反馈
首选当然是通过后台网络默默反馈(在WIFI环境下,避免消耗用户流量)。
如果是以文件为单位上传反馈,只要做好锁文件和销毁文件即可。
如果是在线实时上传反馈,就需要为每次崩溃编号,或根据编号依次上传,或在后台进行合并过滤。
需要注意的是,本地日志文件不能过大,如果超过一定大小限制,要有自动清理机制,比如删除日期最早的那个文件,是的,强烈建议根据日期来建立多个日志文件。