当开发了一个APP,发布出去之后难免会碰到系统异常的情况,而且大多数的异常是不可重现的。经常遇到的情况就是平台迁移之后,数据与原来存在差异,比如说实际情况种
的数据和开发和测试不一样。还有就是测试覆盖面不全,市面上的手机型号那么多,而有的公司并不会为没款手机都适配,跑不同的手机出现异常在所难免。这个时候如果出现
了异常调试起来时比较麻烦的,因为出现异常的环境不一定就能重现出来。
这里主要总结一下开发中用到的异常捕获和调试方法,不同于开发中使用的logcat。思路和原理都很简单,就是当应用发生 异常的时候,可以把异常信息收集并保存起来,我这
里是把它保存到了一个固定目录下的文件中,出现异常时可以通过异常记录文件跟踪定位异常,分析出错原因。
捕获异常的方法是通过实现UncaughtExceptionHandler接口,当异常发生的时候,会回调uncaughtException()方法,在此方法中完成我们需要处理的工作,此处仅把出错信息
保存到固定目录中。
1.首先定义一个自己的异常处理类,实现UncaughtExceptionHandler接口,代码如下:
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
private static ExceptionHandler instance = new ExceptionHandler();
private Context context;
private String infoPath = "/ErrorLog/";
private Thread.UncaughtExceptionHandler defaultHandler;
private Map<String, String> devInfos = new HashMap<String, String>();
private DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
public static ExceptionHandler getInstance() {
return instance;
}
public void setCustomCrashHanler(Context ctx) {
context = ctx;
defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* @name uncaughtException(Thread thread, Throwable ex)
* @description 当发生UncaughtException时会回调此函数
* @param thread 发生异常的线程
* @param ex 抛出的异常
* @return void
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
boolean isDone = doException(ex);
if (!isDone && defaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处理
defaultHandler.uncaughtException(thread, ex);
} else {
// 如果自己处理了异常,则不会弹出错误对话框,则需要手动退出app
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
}
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* @name doException(Throwable ex)
* @description 处理异常
* @param ex 抛出的异常
* @return 异常处理标志
*/
private boolean doException(Throwable ex) {
if (ex == null) {
return true;
}
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(context, "程序出现错误退出!", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
collectDeviceInfo(context);
saveExceptionToFile(ex);
return true;
}
/**
* @name collectDeviceInfo(Context ctx)
* @description 收集必须的设备信息
* @param ctx
* @return void
*/
public void collectDeviceInfo(Context ctx) {
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
devInfos.put("versionName", pi.versionName);
devInfos.put("versionCode", "" + pi.versionCode);
devInfos.put("MODEL", "" + Build.MODEL);
devInfos.put("SDK_INT", "" + Build.VERSION.SDK_INT);
devInfos.put("PRODUCT", "" + Build.PRODUCT);
devInfos.put("TIME", "" + getCurrentTime());
}
} catch (NameNotFoundException e) {
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
devInfos.put(field.getName(), field.get(null).toString());
} catch (Exception e) {
}
}
}
/**
* @name saveExceptionToFile(Throwable ex)
* @description 保存异常信息到文件中
* @param ex 抛出的异常
* @return void
*/
private void saveExceptionToFile(Throwable ex) {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : devInfos.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 {
String time = df.format(new Date());
String fileName = time + ".txt";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String path = Environment.getExternalStorageDirectory() + infoPath;
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(path + fileName);
fos.write(sb.toString().getBytes());
fos.close();
}
} catch (Exception e) {
}
}
/**
* @name getCurrentTime()
* @description 获取当前时间
* @param void
* @return 当前时间
*/
public static String getCurrentTime() {
SimpleDateFormat sdf = null;
sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String CurrentTime = sdf.format(new Date());
return CurrentTime;
}
}
2.在自定义Application初始化异常捕获对象,并设置异常出现是处理的对象:
/**
* @name MyApplication
* @description 自定义Application
* @author cold
* @date 2016.05.18 15:30
* @copyright
*/
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ExceptionHandler mCustomCrashHandler = ExceptionHandler.getInstance();
mCustomCrashHandler.setCustomCrashHanler(this);
}
}
3.在MainActivity里模拟出现空指针的异常
public class MainActivity extends Activity {
private Button btnTest = null;
private Button btnError = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnTest = (Button)findViewById(R.id.btn_test);
btnTest.setOnClickListener(listener);
}
private OnClickListener listener = new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
btnError.setText("error");
}
};
}
代码中btnError并未初始化,所以会报出空指针异常。
4.当出现异常时,可以查看/storage/sdcard0/ErrorLog目录下的异常文件,根据此文件分析异常原因:
文件内容截取:
java.lang.NullPointerException
at com.example.exceptiontest.MainActivity$1.onClick(MainActivity.java:26)
at android.view.View.performClick(View.java:4496)
at android.view.View$PerformClick.run(View.java:18603)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5426)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1268)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1084)
at dalvik.system.NativeStart.main(Native Method)
可以看出这和我们使用logcat调试一样,只不过是多了一步文件存储的过程。
4.需要在AndroidManifest.xml中添加sd卡权限
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
例子代码: