大家都知道,現在安裝Android系統的手機版本和設備千差萬別,在模擬器上運行良好的程序安裝到某款手機上說不定就出現崩潰的現象,開發者個人不可能購買所有設備逐個調試,所以在程序發佈出去之後,如果出現了崩潰現象,開發者應該及時獲取在該設備上導致崩潰的信息,這對於下一個版本的bug修復幫助極大,所以今天就來介紹一下如何在程序崩潰的情況下收集相關的設備參數信息和具體的異常信息,併發送這些信息到服務器供開發者分析和調試程序。
源碼下載地址:http://download.csdn.net/detail/weidi1989/4588310
我們先建立一個crash項目,項目結構如圖:
在MainActivity.java代碼中,故意製作一個錯誤的例子,以便於我們實驗:
- package com.scott.crash;
- import android.app.Activity;
- import android.os.Bundle;
- public class MainActivity extends Activity {
- private String s;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- System.out.println(s.equals("any string"));
- }
- }
我們在這裏故意製造了一個潛在的運行期異常,當我們運行程序時就會出現以下界面:
遇到軟件沒有捕獲的異常之後,系統會彈出這個默認的強制關閉對話框。
我們當然不希望用戶看到這種現象,簡直是對用戶心靈上的打擊,而且對我們的bug的修復也是毫無幫助的。我們需要的是軟件有一個全局的異常捕獲器,當出現一個我們沒有發現的異常時,捕獲這個異常,並且將異常信息記錄下來,上傳到服務器公開發這分析出現異常的具體原因。
接下來我們就來實現這一機制,不過首先我們還是來了解以下兩個類:android.app.Application和java.lang.Thread.UncaughtExceptionHandler。
Application:用來管理應用程序的全局狀態。在應用程序啓動時Application會首先創建,然後纔會根據情況(Intent)來啓動相應的Activity和Service。本示例中將在自定義加強版的Application中註冊未捕獲異常處理器。
Thread.UncaughtExceptionHandler:線程未捕獲異常處理器,用來處理未捕獲異常。如果程序出現了未捕獲異常,默認會彈出系統中強制關閉對話框。我們需要實現此接口,並註冊爲程序中默認未捕獲異常處理。這樣當未捕獲異常發生時,就可以做一些個性化的異常處理操作。
大家剛纔在項目的結構圖中看到的CrashHandler.java實現了Thread.UncaughtExceptionHandler,使我們用來處理未捕獲異常的主要成員,代碼如下:
- package com.way.crash;
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- import java.io.IOException;
- 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.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;
- /**
- * UncaughtException處理類,當程序發生Uncaught異常的時候,由該類來接管程序,並記錄發送錯誤報告.
- *
- * @author way
- *
- */
- public class CrashHandler implements UncaughtExceptionHandler {
- private static final String TAG = "CrashHandler";
- private Thread.UncaughtExceptionHandler mDefaultHandler;// 系統默認的UncaughtException處理類
- private static CrashHandler INSTANCE = new CrashHandler();// CrashHandler實例
- private Context mContext;// 程序的Context對象
- private Map<String, String> info = new HashMap<String, String>();// 用來存儲設備信息和異常信息
- private SimpleDateFormat format = new SimpleDateFormat(
- "yyyy-MM-dd-HH-mm-ss");// 用於格式化日期,作爲日誌文件名的一部分
- /** 保證只有一個CrashHandler實例 */
- private CrashHandler() {
- }
- /** 獲取CrashHandler實例 ,單例模式 */
- public static CrashHandler getInstance() {
- return INSTANCE;
- }
- /**
- * 初始化
- *
- * @param context
- */
- public void init(Context context) {
- mContext = context;
- mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();// 獲取系統默認的UncaughtException處理器
- Thread.setDefaultUncaughtExceptionHandler(this);// 設置該CrashHandler爲程序的默認處理器
- }
- /**
- * 當UncaughtException發生時會轉入該重寫的方法來處理
- */
- public void uncaughtException(Thread thread, Throwable ex) {
- if (!handleException(ex) && mDefaultHandler != null) {
- // 如果自定義的沒有處理則讓系統默認的異常處理器來處理
- mDefaultHandler.uncaughtException(thread, ex);
- } else {
- try {
- Thread.sleep(3000);// 如果處理了,讓程序繼續運行3秒再退出,保證文件保存並上傳到服務器
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 退出程序
- android.os.Process.killProcess(android.os.Process.myPid());
- System.exit(1);
- }
- }
- /**
- * 自定義錯誤處理,收集錯誤信息 發送錯誤報告等操作均在此完成.
- *
- * @param ex
- * 異常信息
- * @return true 如果處理了該異常信息;否則返回false.
- */
- public boolean handleException(Throwable ex) {
- if (ex == null)
- return false;
- new Thread() {
- public void run() {
- Looper.prepare();
- Toast.makeText(mContext, "很抱歉,程序出現異常,即將退出", 0).show();
- Looper.loop();
- }
- }.start();
- // 收集設備參數信息
- collectDeviceInfo(mContext);
- // 保存日誌文件
- saveCrashInfo2File(ex);
- return true;
- }
- /**
- * 收集設備參數信息
- *
- * @param context
- */
- public void collectDeviceInfo(Context context) {
- try {
- PackageManager pm = context.getPackageManager();// 獲得包管理器
- PackageInfo pi = pm.getPackageInfo(context.getPackageName(),
- PackageManager.GET_ACTIVITIES);// 得到該應用的信息,即主Activity
- if (pi != null) {
- String versionName = pi.versionName == null ? "null"
- : pi.versionName;
- String versionCode = pi.versionCode + "";
- info.put("versionName", versionName);
- info.put("versionCode", versionCode);
- }
- } catch (NameNotFoundException e) {
- e.printStackTrace();
- }
- Field[] fields = Build.class.getDeclaredFields();// 反射機制
- for (Field field : fields) {
- try {
- field.setAccessible(true);
- info.put(field.getName(), field.get("").toString());
- Log.d(TAG, field.getName() + ":" + field.get(""));
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
- }
- }
- private String saveCrashInfo2File(Throwable ex) {
- StringBuffer sb = new StringBuffer();
- for (Map.Entry<String, String> entry : info.entrySet()) {
- String key = entry.getKey();
- String value = entry.getValue();
- sb.append(key + "=" + value + "\r\n");
- }
- Writer writer = new StringWriter();
- PrintWriter pw = new PrintWriter(writer);
- ex.printStackTrace(pw);
- Throwable cause = ex.getCause();
- // 循環着把所有的異常信息寫入writer中
- while (cause != null) {
- cause.printStackTrace(pw);
- cause = cause.getCause();
- }
- pw.close();// 記得關閉
- String result = writer.toString();
- sb.append(result);
- // 保存文件
- long timetamp = System.currentTimeMillis();
- String time = format.format(new Date());
- String fileName = "crash-" + time + "-" + timetamp + ".log";
- if (Environment.getExternalStorageState().equals(
- Environment.MEDIA_MOUNTED)) {
- try {
- File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "crash");
- Log.i("CrashHandler", dir.toString());
- if (!dir.exists())
- dir.mkdir();
- FileOutputStream fos = new FileOutputStream(new File(dir,
- fileName));
- fos.write(sb.toString().getBytes());
- fos.close();
- return fileName;
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return null;
- }
- }
然後,我們需要在應用啓動的時候在Application中註冊一下:
- package com.way.crash;
- import android.app.Application;
- public class CrashApplication extends Application {
- @Override
- public void onCreate() {
- super.onCreate();
- CrashHandler crashHandler = CrashHandler.getInstance();
- crashHandler.init(this);
- }
- }
最後,爲了讓我們的CrashApplication取代android.app.Application的地位,在我們的代碼中生效,我們需要修改AndroidManifest.xml:
- <application android:name=".CrashApplication"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@style/AppTheme" >
- ...
- </application>
因爲我們上面的CrashHandler中,遇到異常後要保存設備參數和具體異常信息到SDCARD,所以我們需要在AndroidManifest.xml中加入讀寫SDCARD權限:
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
搞定了上邊的步驟之後,我們來運行一下這個項目:
可以看到,並不會有強制關閉的對話框出現了,取而代之的是我們比較有好的提示信息。
然後看一下SDCARD生成的文件:
用文本編輯器打開日誌文件,看一段日誌信息:
- CPU_ABI=armeabi
- CPU_ABI2=unknown
- ID=FRF91
- MANUFACTURER=unknown
- BRAND=generic
- TYPE=eng
- ......
- Caused by: java.lang.NullPointerException
- at com.scott.crash.MainActivity.onCreate(MainActivity.java:13)
- at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
- at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)
- ... 11 more
這些信息對於開發者來說幫助極大,所以我們需要將此日誌文件上傳到服務器。
下面是一個以郵件形式提交錯誤報告的方法(2013年06月06日新增):
由於context爲非Activity的context,所以,我把彈出的對話框用了系統windows屬性,記得加上以下權限:
- <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
- /**
- * UncaughtException處理類,當程序發生Uncaught異常的時候,由該類來接管程序,並記錄發送錯誤報告.
- *
- * @author way
- *
- */
- public class CrashHandler implements UncaughtExceptionHandler {
- private Thread.UncaughtExceptionHandler mDefaultHandler;// 系統默認的UncaughtException處理類
- private static CrashHandler INSTANCE;// CrashHandler實例
- private Context mContext;// 程序的Context對象
- /** 保證只有一個CrashHandler實例 */
- private CrashHandler() {
- }
- /** 獲取CrashHandler實例 ,單例模式 */
- public static CrashHandler getInstance() {
- if (INSTANCE == null)
- INSTANCE = new CrashHandler();
- return INSTANCE;
- }
- /**
- * 初始化
- *
- * @param context
- */
- public void init(Context context) {
- mContext = context;
- mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();// 獲取系統默認的UncaughtException處理器
- Thread.setDefaultUncaughtExceptionHandler(this);// 設置該CrashHandler爲程序的默認處理器
- }
- /**
- * 當UncaughtException發生時會轉入該重寫的方法來處理
- */
- public void uncaughtException(Thread thread, Throwable ex) {
- if (!handleException(ex) && mDefaultHandler != null) {
- // 如果自定義的沒有處理則讓系統默認的異常處理器來處理
- mDefaultHandler.uncaughtException(thread, ex);
- }
- }
- /**
- * 自定義錯誤處理,收集錯誤信息 發送錯誤報告等操作均在此完成.
- *
- * @param ex
- * 異常信息
- * @return true 如果處理了該異常信息;否則返回false.
- */
- public boolean handleException(Throwable ex) {
- if (ex == null || mContext == null)
- return false;
- final String crashReport = getCrashReport(mContext, ex);
- Log.i("error", crashReport);
- new Thread() {
- public void run() {
- Looper.prepare();
- File file = save2File(crashReport);
- sendAppCrashReport(mContext, crashReport, file);
- Looper.loop();
- }
- }.start();
- return true;
- }
- private File save2File(String crashReport) {
- // TODO Auto-generated method stub
- String fileName = "crash-" + System.currentTimeMillis() + ".txt";
- if (Environment.getExternalStorageState().equals(
- Environment.MEDIA_MOUNTED)) {
- try {
- File dir = new File(Environment.getExternalStorageDirectory()
- .getAbsolutePath() + File.separator + "crash");
- if (!dir.exists())
- dir.mkdir();
- File file = new File(dir, fileName);
- FileOutputStream fos = new FileOutputStream(file);
- fos.write(crashReport.toString().getBytes());
- fos.close();
- return file;
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return null;
- }
- private void sendAppCrashReport(final Context context,
- final String crashReport, final File file) {
- // TODO Auto-generated method stub
- AlertDialog mDialog = null;
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setIcon(android.R.drawable.ic_dialog_info);
- builder.setTitle("程序出錯啦");
- builder.setMessage("請把錯誤報告以郵件的形式提交給我們,謝謝!");
- builder.setPositiveButton(android.R.string.ok,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- // 發送異常報告
- try {
- //註釋部分是已文字內容形式發送錯誤信息
- // Intent intent = new Intent(Intent.ACTION_SENDTO);
- // intent.setType("text/plain");
- // intent.putExtra(Intent.EXTRA_SUBJECT,
- // "推聊Android客戶端 - 錯誤報告");
- // intent.putExtra(Intent.EXTRA_TEXT, crashReport);
- // intent.setData(Uri
- // .parse("mailto:[email protected]"));
- // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // context.startActivity(intent);
- //下面是以附件形式發送郵件
- Intent intent = new Intent(Intent.ACTION_SEND);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- String[] tos = { "[email protected]" };
- intent.putExtra(Intent.EXTRA_EMAIL, tos);
- intent.putExtra(Intent.EXTRA_SUBJECT,
- "推聊Android客戶端 - 錯誤報告");
- if (file != null) {
- intent.putExtra(Intent.EXTRA_STREAM,
- Uri.fromFile(file));
- intent.putExtra(Intent.EXTRA_TEXT,
- "請將此錯誤報告發送給我,以便我儘快修復此問題,謝謝合作!\n");
- } else {
- intent.putExtra(Intent.EXTRA_TEXT,
- "請將此錯誤報告發送給我,以便我儘快修復此問題,謝謝合作!\n"
- + crashReport);
- }
- intent.setType("text/plain");
- intent.setType("message/rfc882");
- Intent.createChooser(intent, "Choose Email Client");
- context.startActivity(intent);
- } catch (Exception e) {
- Toast.makeText(context,
- "There are no email clients installed.",
- Toast.LENGTH_SHORT).show();
- } finally {
- dialog.dismiss();
- // 退出
- android.os.Process.killProcess(android.os.Process
- .myPid());
- System.exit(1);
- }
- }
- });
- builder.setNegativeButton(android.R.string.cancel,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- // 退出
- android.os.Process.killProcess(android.os.Process
- .myPid());
- System.exit(1);
- }
- });
- mDialog = builder.create();
- mDialog.getWindow().setType(
- WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
- mDialog.show();
- }
- /**
- * 獲取APP崩潰異常報告
- *
- * @param ex
- * @return
- */
- private String getCrashReport(Context context, Throwable ex) {
- PackageInfo pinfo = getPackageInfo(context);
- StringBuffer exceptionStr = new StringBuffer();
- exceptionStr.append("Version: " + pinfo.versionName + "("
- + pinfo.versionCode + ")\n");
- exceptionStr.append("Android: " + android.os.Build.VERSION.RELEASE
- + "(" + android.os.Build.MODEL + ")\n");
- exceptionStr.append("Exception: " + ex.getMessage() + "\n");
- StackTraceElement[] elements = ex.getStackTrace();
- for (int i = 0; i < elements.length; i++) {
- exceptionStr.append(elements[i].toString() + "\n");
- }
- return exceptionStr.toString();
- }
- /**
- * 獲取App安裝包信息
- *
- * @return
- */
- private PackageInfo getPackageInfo(Context context) {
- PackageInfo info = null;
- try {
- info = context.getPackageManager().getPackageInfo(
- context.getPackageName(), 0);
- } catch (NameNotFoundException e) {
- // e.printStackTrace(System.err);
- // L.i("getPackageInfo err = " + e.getMessage());
- }
- if (info == null)
- info = new PackageInfo();
- return info;
- }
- }