Android:處理程序崩潰異常

你們都知道,如今安裝Android系統的手機版本和設備千差萬別,在模擬器上運行良好的程序安裝到某款手機上說不定就出現崩潰的現象,開發者我的不可能購買全部設備逐個調試,因此在程序發佈出去以後,若是出現了崩潰現象,開發者應該及時獲取在該設備上致使崩潰的信息,這對於下一個版本的bug修復幫助極大,因此今天就來介紹一下如何在程序崩潰的狀況下收集相關的設備參數信息和具體的異常信息,併發送這些信息到服務器供開發者分析和調試程序。html

咱們先創建一個crash項目,項目結構如圖:java

在MainActivity.java代碼中,代碼是這樣寫的:android

[plain]  view plain copy print ?
  1. package com.scott.crash;    
  2.     
  3. import android.app.Activity;    
  4. import android.os.Bundle;    
  5.     
  6. public class MainActivity extends Activity {    
  7.     
  8.     private String s;    
  9.         
  10.     @Override    
  11.     public void onCreate(Bundle savedInstanceState) {    
  12.         super.onCreate(savedInstanceState);    
  13.         System.out.println(s.equals("any string"));    
  14.     }    
  15. }    


 咱們在這裏故意製造了一個潛在的運行期異常,當咱們運行程序時就會出現如下界面:服務器

遇到軟件沒有捕獲的異常以後,系統會彈出這個默認的強制關閉對話框。網絡

咱們固然不但願用戶看到這種現象,簡直是對用戶心靈上的打擊,並且對咱們的bug的修復也是毫無幫助的。咱們須要的是軟件有一個全局的異常捕獲器,當出現一個咱們沒有發現的異常時,捕獲這個異常,而且將異常信息記錄下來,上傳到服務器公開發這分析出現異常的具體緣由。併發

接下來咱們就來實現這一機制,不過首先咱們仍是來了解如下兩個類:android.app.Application和java.lang.Thread.UncaughtExceptionHandler。app

Application:用來管理應用程序的全局狀態。在應用程序啓動時Application會首先建立,而後纔會根據狀況(Intent)來啓動相應的Activity和Service。本示例中將在自定義增強版的Application中註冊未捕獲異常處理器。編輯器

Thread.UncaughtExceptionHandler:線程未捕獲異常處理器,用來處理未捕獲異常。若是程序出現了未捕獲異常,默認會彈出系統中強制關閉對話框。咱們須要實現此接口,並註冊爲程序中默認未捕獲異常處理。這樣當未捕獲異常發生時,就能夠作一些個性化的異常處理操做。ide

你們剛纔在項目的結構圖中看到的CrashHandler.java實現了Thread.UncaughtExceptionHandler,使咱們用來處理未捕獲異常的主要成員,代碼以下:函數

[java]  view plain copy print ?
  1. package com.scott.crash;    
  2.     
  3. import java.io.File;    
  4. import java.io.FileOutputStream;    
  5. import java.io.PrintWriter;    
  6. import java.io.StringWriter;    
  7. import java.io.Writer;    
  8. import java.lang.Thread.UncaughtExceptionHandler;    
  9. import java.lang.reflect.Field;    
  10. import java.text.DateFormat;    
  11. import java.text.SimpleDateFormat;    
  12. import java.util.Date;    
  13. import java.util.HashMap;    
  14. import java.util.Map;    
  15.     
  16. import android.content.Context;    
  17. import android.content.pm.PackageInfo;    
  18. import android.content.pm.PackageManager;    
  19. import android.content.pm.PackageManager.NameNotFoundException;    
  20. import android.os.Build;    
  21. import android.os.Environment;    
  22. import android.os.Looper;    
  23. import android.util.Log;    
  24. import android.widget.Toast;    
  25.     
  26. /**  
  27.  * UncaughtException處理類,當程序發生Uncaught異常的時候,有該類來接管程序,並記錄發送錯誤報告.  
  28.  *   
  29.  * @author user  
  30.  *   
  31.  */    
  32. public class CrashHandler implements UncaughtExceptionHandler {    
  33.         
  34.     public static final String TAG = "CrashHandler";    
  35.         
  36.     //系統默認的UncaughtException處理類     
  37.     private Thread.UncaughtExceptionHandler mDefaultHandler;    
  38.     //CrashHandler實例    
  39.     private static CrashHandler INSTANCE = new CrashHandler();    
  40.     //程序的Context對象    
  41.     private Context mContext;    
  42.     //用來存儲設備信息和異常信息    
  43.     private Map<String, String> infos = new HashMap<String, String>();    
  44.     
  45.     //用於格式化日期,做爲日誌文件名的一部分    
  46.     private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");    
  47.     
  48.     /** 保證只有一個CrashHandler實例 */    
  49.     private CrashHandler() {    
  50.     }    
  51.     
  52.     /** 獲取CrashHandler實例 ,單例模式 */    
  53.     public static CrashHandler getInstance() {    
  54.         return INSTANCE;    
  55.     }    
  56.     
  57.     /**  
  58.      * 初始化  
  59.      *   
  60.      * @param context  
  61.      */    
  62.     public void init(Context context) {    
  63.         mContext = context;    
  64.         //獲取系統默認的UncaughtException處理器    
  65.         mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();    
  66.         //設置該CrashHandler爲程序的默認處理器    
  67.         Thread.setDefaultUncaughtExceptionHandler(this);    
  68.     }    
  69.     
  70.     /**  
  71.      * 當UncaughtException發生時會轉入該函數來處理  
  72.      */    
  73.     @Override    
  74.     public void uncaughtException(Thread thread, Throwable ex) {    
  75.         if (!handleException(ex) && mDefaultHandler != null) {    
  76.             //若是用戶沒有處理則讓系統默認的異常處理器來處理    
  77.             mDefaultHandler.uncaughtException(thread, ex);    
  78.         } else {    
  79.             try {    
  80.                 Thread.sleep(3000);    
  81.             } catch (InterruptedException e) {    
  82.                 Log.e(TAG, "error : ", e);    
  83.             }    
  84.             //退出程序    
  85.             android.os.Process.killProcess(android.os.Process.myPid());    
  86.             System.exit(1);    
  87.         }    
  88.     }    
  89.     
  90.     /**  
  91.      * 自定義錯誤處理,收集錯誤信息 發送錯誤報告等操做均在此完成.  
  92.      *   
  93.      * @param ex  
  94.      * @return true:若是處理了該異常信息;不然返回false.  
  95.      */    
  96.     private boolean handleException(Throwable ex) {    
  97.         if (ex == null) {    
  98.             return false;    
  99.         }    
  100.         //使用Toast來顯示異常信息    
  101.         new Thread() {    
  102.             @Override    
  103.             public void run() {    
  104.                 Looper.prepare();    
  105.                 Toast.makeText(mContext, "很抱歉,程序出現異常,即將退出.", Toast.LENGTH_LONG).show();    
  106.                 Looper.loop();    
  107.             }    
  108.         }.start();    
  109.         //收集設備參數信息     
  110.         collectDeviceInfo(mContext);    
  111.         //保存日誌文件     
  112.         saveCrashInfo2File(ex);    
  113.         return true;    
  114.     }    
  115.         
  116.     /**  
  117.      * 收集設備參數信息  
  118.      * @param ctx  
  119.      */    
  120.     public void collectDeviceInfo(Context ctx) {    
  121.         try {    
  122.             PackageManager pm = ctx.getPackageManager();    
  123.             PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);    
  124.             if (pi != null) {    
  125.                 String versionName = pi.versionName == null ? "null" : pi.versionName;    
  126.                 String versionCode = pi.versionCode + "";    
  127.                 infos.put("versionName", versionName);    
  128.                 infos.put("versionCode", versionCode);    
  129.             }    
  130.         } catch (NameNotFoundException e) {    
  131.             Log.e(TAG, "an error occured when collect package info", e);    
  132.         }    
  133.         Field[] fields = Build.class.getDeclaredFields();    
  134.         for (Field field : fields) {    
  135.             try {    
  136.                 field.setAccessible(true);    
  137.                 infos.put(field.getName(), field.get(null).toString());    
  138.                 Log.d(TAG, field.getName() + " : " + field.get(null));    
  139.             } catch (Exception e) {    
  140.                 Log.e(TAG, "an error occured when collect crash info", e);    
  141.             }    
  142.         }    
  143.     }    
  144.     
  145.     /**  
  146.      * 保存錯誤信息到文件中  
  147.      *   
  148.      * @param ex  
  149.      * @return  返回文件名稱,便於將文件傳送到服務器  
  150.      */    
  151.     private String saveCrashInfo2File(Throwable ex) {    
  152.             
  153.         StringBuffer sb = new StringBuffer();    
  154.         for (Map.Entry<String, String> entry : infos.entrySet()) {    
  155.             String key = entry.getKey();    
  156.             String value = entry.getValue();    
  157.             sb.append(key + "=" + value + "\n");    
  158.         }    
  159.             
  160.         Writer writer = new StringWriter();    
  161.         PrintWriter printWriter = new PrintWriter(writer);    
  162.         ex.printStackTrace(printWriter);    
  163.         Throwable cause = ex.getCause();    
  164.         while (cause != null) {    
  165.             cause.printStackTrace(printWriter);    
  166.             cause = cause.getCause();    
  167.         }    
  168.         printWriter.close();    
  169.         String result = writer.toString();    
  170.         sb.append(result);    
  171.         try {    
  172.             long timestamp = System.currentTimeMillis();    
  173.             String time = formatter.format(new Date());    
  174.             String fileName = "crash-" + time + "-" + timestamp + ".log";    
  175.             if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {    
  176.                 String path = "/sdcard/crash/";    
  177.                 File dir = new File(path);    
  178.                 if (!dir.exists()) {    
  179.                     dir.mkdirs();    
  180.                 }    
  181.                 FileOutputStream fos = new FileOutputStream(path + fileName);    
  182.                 fos.write(sb.toString().getBytes());    
  183.                 fos.close();    
  184.             }    
  185.             return fileName;    
  186.         } catch (Exception e) {    
  187.             Log.e(TAG, "an error occured while writing file...", e);    
  188.         }    
  189.         return null;    
  190.     }    
  191. }    

在收集異常信息時,朋友們也可使用Properties,由於Properties有一個很便捷的方法properties.store(OutputStream out, String comments),用來將Properties實例中的鍵值對外輸到輸出流中,可是在使用的過程當中發現生成的文件中異常信息打印在同一行,看起來極爲費勁,因此換成Map來存放這些信息,而後生成文件時稍加了些操做。

完成這個CrashHandler後,咱們須要在一個Application環境中讓其運行,爲此,咱們繼承android.app.Application,添加本身的代碼,CrashApplication.java代碼以下:

[java]  view plain copy print ?
  1. package com.scott.crash;    
  2.     
  3. import android.app.Application;    
  4.     
  5. public class CrashApplication extends Application {    
  6.     @Override    
  7.     public void onCreate() {    
  8.         super.onCreate();    
  9.         CrashHandler crashHandler = CrashHandler.getInstance();    
  10.         crashHandler.init(getApplicationContext());    
  11.     }    
  12. }    

最後,爲了讓咱們的CrashApplication取代android.app.Application的地位,在咱們的代碼中生效,咱們須要修改AndroidManifest.xml:

[java]  view plain copy print ?
  1. <application android:name=".CrashApplication" ...>    
  2. </application>    

由於咱們上面的CrashHandler中,遇到異常後要保存設備參數和具體異常信息到SDCARD,因此咱們須要在AndroidManifest.xml中加入讀寫SDCARD權限:

[java]  view plain copy print ?
  1. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    

搞定了上邊的步驟以後,咱們來運行一下這個項目:

看以看到,並不會有強制關閉的對話框出現了,取而代之的是咱們比較有好的提示信息。

而後看一下SDCARD生成的文件:



用文本編輯器打開日誌文件,看一段日誌信息:

[plain]  view plain copy print ?
  1. CPU_ABI=armeabi    
  2. CPU_ABI2=unknown    
  3. ID=FRF91    
  4. MANUFACTURER=unknown    
  5. BRAND=generic    
  6. TYPE=eng    
  7. ......    
  8. Caused by: java.lang.NullPointerException    
  9.     at com.scott.crash.MainActivity.onCreate(MainActivity.java:13)    
  10.     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)    
  11.     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)    
  12.     ... 11 more    


這些信息對於開發者來講幫助極大,因此咱們須要將此日誌文件上傳到服務器,有關文件上傳的技術,請參照Android中使用HTTP服務相關介紹。

不過在使用HTTP服務以前,須要肯定網絡暢通,咱們可使用下面的方式判斷網絡是否可用:

[plain]  view plain copy print ?
  1. /**   
  2.      * 網絡是否可用   
  3.      *    
  4.      * @param context   
  5.      * @return   
  6.      */    
  7.     public static boolean isNetworkAvailable(Context context) {    
  8.         ConnectivityManager mgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);    
  9.         NetworkInfo[] info = mgr.getAllNetworkInfo();    
  10.         if (info != null) {    
  11.             for (int i = 0; i < info.length; i++) {    
  12.                 if (info[i].getState() == NetworkInfo.State.CONNECTED) {    
  13.                     return true;    
  14.                 }    
  15.             }    
  16.         }    
  17.         return false;    
  18.     }    
相關文章
相關標籤/搜索