在实际项目开发中,会出现很多的异常直接导致程序crash掉,在开发中我们可以通过logcat查看错误日志,Debug出现的异常,让程序安全的运行,但是在开发中有些异常隐藏的比较深,直到项目发布后,由于各种原因,譬如Android设备不一致等等,android版本不同,实际上我们在测试的时候不可能在市场上所有的Android设备上都做了测试,当用户安装使用时被暴露出来,导致程序直接crash掉,这显然对于用户是不OK的!这些在用户设备上导致crash的异常我们是不知道的,要想知道这些异常出现的一些信息,我们还是得自己通过程序捕获到异常,并且将其记录下来(本地保存或者上传服务器),方便项目维护。
先来看一下,我自己“故意”定义出来的一个异常,在MainActivity,java中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.crash; import android.os.Bundle ; import android.app.Activity ; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R .layout.activity_main); int i = 1 ; System .out.println(i/0 ); } }
以上程序报出一个数学运算的除0异常,显然程序被“崩溃”,不能继续执行的。看一下运行效果截图: 运行结果如上图所示,这种直接崩溃的效果对于用户来说是很不OK的,用户不知道发生了什么,程序就停止了,会让用户对程序有种不想继续使用的想法。 对于程序中未捕获的异常,我们可以做哪些操作呢!我们需要的是软件有一个全局的异常捕获器,当出现一个我们没有发现的异常时,捕获这个异常,并且将异常信息记录下来,上传到服务器公开发这分析出现异常的具体原因,这是一种最佳实践,那么我们接下来就必须要熟悉两个类别,一个是android提供的Application,另一个是Java提供的Thread.UncaughtExceptionHandler。
Application:这是android程序管理全局状态的类,Application在程序启动的时候首先被创建出来,它被用来统一管理activity、service、broadcastreceiver、contentprovider四大组件以及其他android元素,这里可以打开android工程下的Mainifest.xml文件查看一下。我们除了使用android默认的Application来处理程序,也可以自定义一个Application处理一些需要在全局状态下控制程序的操作,例如本文讲到的处理程序未知异常时,这是一种最佳实践。
Thread.UncaughtExceptionHandler:关于这个概念的解释,我在JDK1.6的文档中找到一些科学的解释。
当 Thread 因未捕获的异常而突然终止时,调用处理程序的接口。 当某一线程因未捕获的异常而即将终止时,Java 虚拟机将使用Thread.getUncaughtExceptionHandler()
查询该线程以获得其 UncaughtExceptionHandler 的线程,并调用处理程序的 uncaughtException 方法,将线程和异常作为参数传递。如果某一线程没有明确设置其 UncaughtExceptionHandler,则将它的 ThreadGroup 对象作为其 UncaughtExceptionHandler。如果 ThreadGroup 对象对处理异常没有什么特殊要求,那么它可以将调用转发给默认的未捕获异常处理程序。
Thread.UncaughtExceptionHandler是一个接口,它提供如下的方法,让我们自定义处理程序。
void uncaughtException(Thread t,Throwable e)
当给定线程因给定的未捕获异常而终止时,调用该方法。Java 虚拟机将忽略该方法抛出的任何异常。参数:t - 线程 e - 异常
一句话,线程未捕获异常处理器,用来处理未捕获异常。如果程序出现了未捕获异常,默认会弹出系统中强制关闭对话框。我们需要实现此接口,并注册为程序中默认未捕获异常处理。这样当未捕获异常发生时,就可以做一些个性化的异常处理操作。所以接下来,我们要做的就是自定义一个CrashHandler类去实现Thread.UncaughtExceptionHandler,并且在实现的方法中做一些相关的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 package com.example.crash; import java.io.File; import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.Thread.UncaughtExceptionHandler; import java.lang.reflect.Field; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.Environment; import android.os.Looper; import android.util.Log; import android.widget.Toast; public class CrashHandler implements UncaughtExceptionHandler { public static final String TAG = "CrashHandler" ; private Thread.UncaughtExceptionHandler mDefaultHandler; private static CrashHandler INSTANCE = new CrashHandler (); private Context mContext; private Map<String, String> infos = new HashMap <String, String>(); private DateFormat formatter = new SimpleDateFormat ("yyyy-MM-dd-HH-mm-ss" ); private CrashHandler () { } public static CrashHandler getInstance () { return INSTANCE; } public void init (Context context) { mContext = context; mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this ); } @Override public void uncaughtException (Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null ) { mDefaultHandler.uncaughtException(thread, ex); } else { try { Thread.sleep(3000 ); } catch (InterruptedException e) { Log.e(TAG, "error : " , e); } android.os.Process.killProcess(android.os.Process.myPid()); System.exit(1 ); } } private boolean handleException (Throwable ex) { if (ex == null ) { return false ; } new Thread () { @Override public void run () { Looper.prepare(); Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出." , Toast.LENGTH_LONG) .show(); Looper.loop(); } }.start(); collectDeviceInfo(mContext); saveCrashInfo2File(ex); return true ; } public void collectDeviceInfo (Context ctx) { try { PackageManager pm = ctx.getPackageManager(); PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES); if (pi != null ) { String versionName = pi.versionName == null ? "null" : pi.versionName; String versionCode = pi.versionCode + "" ; infos.put("versionName" , versionName); infos.put("versionCode" , versionCode); } } catch (NameNotFoundException e) { Log.e(TAG, "an error occured when collect package info" , e); } Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true ); infos.put(field.getName(), field.get(null ).toString()); Log.d(TAG, field.getName() + " : " + field.get(null )); } catch (Exception e) { Log.e(TAG, "an error occured when collect crash info" , e); } } } private String saveCrashInfo2File (Throwable ex) { 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" ); } 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(); sb.append(result); try { long timestamp = System.currentTimeMillis(); String time = formatter.format(new Date ()); String fileName = "crash-" + time + "-" + timestamp + ".log" ; if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { String path = "/sdcard/crash/" ; File dir = new File (path); if (!dir.exists()) { dir.mkdirs(); } FileOutputStream fos = new FileOutputStream (path + fileName); fos.write(sb.toString().getBytes()); fos.close(); } return fileName; } catch (Exception e) { Log.e(TAG, "an error occured while writing file..." , e); } return null ; } }
完成了这个CrashHandler类之后,还需要自定义一个全局Application来启动管理异常收集,以下是自定义的Application类,很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.crash; import android.app.Application ; public class CrashApplication extends Application { @Override public void onCreate() { super .onCreate(); CrashHandler crashHandler = CrashHandler .getInstance(); crashHandler.init(getApplicationContext()); } }
最后,为了让程序在启动时使用我们自定义的Application,必须在Mainifest.xml的Application节点上,声明出我们自定义的Application:
1 2 3 4 <application android:name =".CrashApplication" ... > ..... </application >
配置SDCard写文件的权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
运行以下程序:
在SD卡中找到crash文件夹,打开文件夹:
到处这个log日志,用notepad打开,查看内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 TIME=1385535270000 ...... java.lang .RuntimeException : Unable to start activity ComponentInfo{com.example .crash/com.example .crash .MainActivity}: java.lang .ArithmeticException : divide by zero ...... Caused by: java.lang .ArithmeticException : divide by zero at com.example .crash .MainActivity .onCreate (MainActivity.java :13 ) at android.app .Activity .performCreate (Activity.java :5243 ) at android.app .Instrumentation .callActivityOnCreate (Instrumentation.java :1087 ) at android.app .ActivityThread .performLaunchActivity (ActivityThread.java :2140 ) ... 11 more java.lang .ArithmeticException : divide by zero at com.example .crash .MainActivity .onCreate (MainActivity.java :13 ) ......
好了,程序中未捕获的异常被及时捕捉到,保存在SD卡中,并且给用户良好的提示信息,被没有一下子crash掉,通过SD卡中的错误日志,我们可以很快定义到错误的根源,方便我们及时对程序进行修正。当然了,这里我由于做的是个Demo,所以相关错误日志仅仅保存在了SD卡上,其实好的做法是将错误日志上传到服务器中,以便我们收集来自四面八方用户的日志,为程序进行更新迭代升级。
注:该文是我学习笔记,里面会有一些Bug。程序仅作为参考实例,不能直接使用到真实项目中,请谅解! 实际项目中可用U盟 或Bugly 收集Crash信息。
参考:Android 重写系统Crash处理类,保存Crash信息到SD卡和完美退出程序的方法 Android借助Application重写App的Crash-完整版 Android应用全局异常处理