UncaughtExceptionHandler接口实现

首先创建一个OPhone项目(项目的创建请参考OPhoneSDN上的其他文章),本文示例项目名称为:CrashReporter ;包名为:org.goodev.cr;并创建一个默认的Activity名字为:ReporterTest。然后创建CrashHandler类实现UncaughtExceptionHandler接口,并实现其函数:public void uncaughtException(Thread thread, Throwable ex)。CrashHandler类实现了错误报告的主要处理逻辑,该类代码如下(在代码中会有详细注释来解释各种处理情况):


package org.goodev.cr;

Import 省略...; 

/** 

 * UncaughtException处理类,当程序发生Uncaught异常的时候,有该类 

 * 来接管程序,并记录 发送错误报告. 

 * 

 */ 

public class CrashHandler implements UncaughtExceptionHandler { 

 /** Debug Log tag*/ 

 public static final String TAG = "CrashHandler"; 

 /** 是否开启日志输出,在Debug状态下开启, 

 * 在Release状态下关闭以提示程序性能 

 * */ 

 public static final boolean DEBUG = true; 

 /** 系统默认的UncaughtException处理类 */ 

 private Thread.UncaughtExceptionHandler mDefaultHandler; 

 /** CrashHandler实例 */ 

 private static CrashHandler INSTANCE; 

 /** 程序的Context对象 */ 

 private Context mContext; 


 /** 使用Properties来保存设备的信息和错误堆栈信息*/ 

 private Properties mDeviceCrashInfo = new Properties(); 

 private static final String VERSION_NAME = "versionName"; 

 private static final String VERSION_CODE = "versionCode"; 

 private static final String STACK_TRACE = "STACK_TRACE"; 

 /** 错误报告文件的扩展名 */ 

 private static final String CRASH_REPORTER_EXTENSION = ".cr"; 


 /** 保证只有一个CrashHandler实例 */ 

 private CrashHandler() {} 

 /** 获取CrashHandler实例 ,单例模式*/ 

 public static CrashHandler getInstance() { 

 if (INSTANCE == null) { 

 INSTANCE = new CrashHandler(); 

 } 

 return INSTANCE; 

 } 


 /** 

 * 初始化,注册Context对象, 

 * 获取系统默认的UncaughtException处理器, 

 * 设置该CrashHandler为程序的默认处理器 

 * 

 * @param ctx 

 */ 

 public void init(Context ctx) { 

 mContext = ctx; 

 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); 

 Thread.setDefaultUncaughtExceptionHandler(this); 

 } 


 /** 

 * 当UncaughtException发生时会转入该函数来处理 

 */ 

 @Override 

 public void uncaughtException(Thread thread, Throwable ex) { 

 if (!handleException(ex) && mDefaultHandler != null) { 

 //如果用户没有处理则让系统默认的异常处理器来处理 

 mDefaultHandler.uncaughtException(thread, ex); 

 } else { 

 //Sleep一会后结束程序 

 try { 

 Thread.sleep(3000); 

 } catch (InterruptedException e) { 

 Log.e(TAG, "Error : ", e); 

 } 

 Android.os.Process.killProcess(android.os.Process.myPid()); 

 System.exit(10); 

 } 

 } 


 /** 

 * 自定义错误处理,收集错误信息 

 * 发送错误报告等操作均在此完成. 

 * 开发者可以根据自己的情况来自定义异常处理逻辑 

 * @param ex 

 * @return true:如果处理了该异常信息;否则返回false 

 */ 

 private boolean handleException(Throwable ex) { 

 if (ex == null) { 

 return true; 

 } 

 final String msg = ex.getLocalizedMessage(); 

 //使用Toast来显示异常信息 

 new Thread() { 

 @Override 

 public void run() { 

 Looper.prepare(); 

 Toast.makeText(mContext, "程序出错啦:" + msg, Toast.LENGTH_LONG) 

 .show(); 

 Looper.loop(); 

 } 


 }.start(); 

 //收集设备信息 

 collectCrashDeviceInfo(mContext); 

 //保存错误报告文件 

 String crashFileName = saveCrashInfoToFile(ex); 

 //发送错误报告到服务器 

 sendCrashReportsToServer(mContext); 

 return true; 

 } 


 /** 

 * 在程序启动时候, 可以调用该函数来发送以前没有发送的报告 

 */ 

 public void sendPreviousReportsToServer() { 

 sendCrashReportsToServer(mContext); 

 } 


 /** 

 * 把错误报告发送给服务器,包含新产生的和以前没发送的. 

 * 

 * @param ctx 

 */ 

 private void sendCrashReportsToServer(Context ctx) { 

 String[] crFiles = getCrashReportFiles(ctx); 

 if (crFiles != null && crFiles.length > 0) { 

 TreeSet<String> sortedFiles = new TreeSet<String>(); 

 sortedFiles.addAll(Arrays.asList(crFiles)); 


 for (String fileName : sortedFiles) { 

 File cr = new File(ctx.getFilesDir(), fileName); 

 postReport(cr); 

 cr.delete();// 删除已发送的报告 

 } 

 } 

 } 


 private void postReport(File file) { 

 // TODO 使用HTTP Post 发送错误报告到服务器 

 // 这里不再详述,开发者可以根据OPhoneSDN上的其他网络操作 

 // 教程来提交错误报告 

 } 


 /** 

 * 获取错误报告文件名 

 * @param ctx 

 * @return 

 */ 

 private String[] getCrashReportFiles(Context ctx) { 

 File filesDir = ctx.getFilesDir(); 

 FilenameFilter filter = new FilenameFilter() { 

 public boolean accept(File dir, String name) { 

 return name.endsWith(CRASH_REPORTER_EXTENSION); 

 } 

 }; 

 return filesDir.list(filter); 

 } 

 /** 

 * 保存错误信息到文件中 

 * @param ex 

 * @return 

 */ 

 private String saveCrashInfoToFile(Throwable ex) { 

 Writer info = new StringWriter(); 

 PrintWriter printWriter = new PrintWriter(info); 

 ex.printStackTrace(printWriter); 


 Throwable cause = ex.getCause(); 

 while (cause != null) { 

 cause.printStackTrace(printWriter); 

 cause = cause.getCause(); 

 } 


 String result = info.toString(); 

 printWriter.close(); 

 mDeviceCrashInfo.put(STACK_TRACE, result); 


 try { 

 long timestamp = System.currentTimeMillis(); 

 String fileName = "crash-" + timestamp + CRASH_REPORTER_EXTENSION; 

 FileOutputStream trace = mContext.openFileOutput(fileName, 

 Context.MODE_PRIVATE); 

 mDeviceCrashInfo.store(trace, ""); 

 trace.flush(); 

 trace.close(); 

 return fileName; 

 } catch (Exception e) { 

 Log.e(TAG, "an error occured while writing report file...", e); 

 } 

 return null; 

 } 



 /** 

 * 收集程序崩溃的设备信息 

 * 

 * @param ctx 

 */ 

 public void collectCrashDeviceInfo(Context ctx) { 

 try { 

 PackageManager pm = ctx.getPackageManager(); 

 PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), 

 PackageManager.GET_ACTIVITIES); 

 if (pi != null) { 

 mDeviceCrashInfo.put(VERSION_NAME, 

 pi.versionName == null ? "not set" : pi.versionName); 

 mDeviceCrashInfo.put(VERSION_CODE, pi.versionCode); 

 } 

 } catch (NameNotFoundException e) { 

 Log.e(TAG, "Error while collect package info", e); 

 } 

 //使用反射来收集设备信息.在Build类中包含各种设备信息, 

 //例如: 系统版本号,设备生产商 等帮助调试程序的有用信息 

//具体信息请参考后面的截图 

 Field[] fields = Build.class.getDeclaredFields(); 

 for (Field field : fields) { 

 try { 

 field.setAccessible(true); 

 mDeviceCrashInfo.put(field.getName(), field.get(null)); 

 if (DEBUG) { 

 Log.d(TAG, field.getName() + " : " + field.get(null)); 

 } 

 } catch (Exception e) { 

 Log.e(TAG, "Error while collect crash info", e); 

 } 


 } 


 } 


}


在上面CrashHandler实现中,当错误发生的时候使用Toast显示错误信息,然后收集错误报告并保存在文件中。 发送错误报告代码请读者自己实现。在uncaughtException函数中调用了Thread.sleep(3000);来让线程停止一会是为了显示Toast信息给用户,然后Kill程序。如果你不用Toast来显示信息则可以去除该代码。除了Toast外,开发者还可以选择使用Notification来显示错误内容并让用户选择是否提交错误报告而不是自动提交。关于Notification的实现请读者参考:NotificationManager。在发送错误报道的时候,可以先检测网络是否可用,如果不可用则可以在以后网络情况可用的情况下发送。 网络监测代码如下:

/** 

 * 检测网络连接是否可用 

 * @param ctx 

 * @return true 可用; false 不可用 

 */ 

 private boolean isNetworkAvailable(Context ctx) { 

 ConnectivityManager cm = 

 (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); 

 if(cm == null) { 

 return false; 

 } 

 NetworkInfo[] netinfo = cm.getAllNetworkInfo(); 

 if(netinfo == null) { 

 return false; 

 } 

 for (int i = 0; i < netinfo.length; i++) { 

 if(netinfo[i].isConnected()) { 

 return true; 

 } 

 } 

 return false; 

 }



下面是在模拟器和笔者手机(Dell mini3i OPhone1.5系统)上收集的具体信息截图:


Application 实现

实现一个自定义Application来注册CrashHandler. 代码如下:

public class CrashApplication extends Application { 


 @Override 

 public void onCreate() { 

 super.onCreate(); 

 CrashHandler crashHandler = CrashHandler.getInstance(); 

 //注册crashHandler 

 crashHandler.init(getApplicationContext()); 

 //发送以前没发送的报告(可选) 

 crashHandler.sendPreviousReportsToServer(); 

 } 


}


在AndroidManifest.xml中注册

最后只要在AndroidManifest.xml中注册CrashApplication就可以了。代码如下:

<?xml version="1.0" encoding="utf-8"?> 

<manifest xmlns:Android="http://schemas.android.com/apk/res/android" 

 package="org.goodev.cr" 

 Android:versionCode="1" 

 Android:versionName="1.0"> 

 <application Android:icon="@drawable/icon" android:label="@string/app_name" 

 Android:name=".CrashApplication"> 

 <activity Android:name=".ReporterTest" 

 Android:label="@string/app_name"> 

 <intent-filter> 

 <action Android:name="android.intent.action.MAIN" /> 

 <category Android:name="android.intent.category.LAUNCHER" /> 

 </intent-filter> 

 </activity> 

 </application> 

</manifest>




总结:通过本文示例的方式,开发者可以在程序中收集详细的崩溃信息,从而为调试程序带来便利,如果您的程序还没有该功能赶快加入吧。crashReporter.zip中包含本文使用的项目文件及资源。