大家都知道,現在安裝Android系統的手機版本和裝置千差萬別,在模擬器上執行良好的程式安裝到某款手機上說不定就出現崩潰的現象,開發者個人不可能購買所有裝置逐個除錯,所以在程式釋出出去之後,如果出現了崩潰現象,開發者應該及時獲取在該裝置上導致崩潰的資訊,這對於下一個版本的bug修復幫助極大,所以今天就來介紹一下如何在程式崩潰的情況下收集相關的裝置引數資訊和具體的異常資訊,併傳送這些資訊到伺服器供開發者分析和除錯程式。
我們先建立一個crash專案,專案結構如圖:
在MainActivity.java程式碼中,程式碼是這樣寫的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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,使我們用來處理未捕獲異常的主要成員,程式碼如下:
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 189 190 191 |
package com.scott.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; /** * UncaughtException處理類,當程式發生Uncaught異常的時候,有該類來接管程式,並記錄傳送錯誤報告. * * @author user * */ public class CrashHandler implements UncaughtExceptionHandler { public static final String TAG = "CrashHandler"; //系統預設的UncaughtException處理類 private Thread.UncaughtExceptionHandler mDefaultHandler; //CrashHandler例項 private static CrashHandler INSTANCE = new CrashHandler(); //程式的Context物件 private Context mContext; //用來儲存裝置資訊和異常資訊 private Map<String, String> infos = new HashMap<String, String>(); //用於格式化日期,作為日誌檔名的一部分 private DateFormat formatter = 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; //獲取系統預設的UncaughtException處理器 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); //設定該CrashHandler為程式的預設處理器 Thread.setDefaultUncaughtExceptionHandler(this); } /** * 當UncaughtException發生時會轉入該函式來處理 */ @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); } } /** * 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成. * * @param ex * @return true:如果處理了該異常資訊;否則返回false. */ private boolean handleException(Throwable ex) { if (ex == null) { return false; } //使用Toast來顯示異常資訊 new Thread() { @Override public void run() { Looper.prepare(); Toast.makeText(mContext, "很抱歉,程式出現異常,即將退出.", Toast.LENGTH_LONG).show(); Looper.loop(); } }.start(); //收集裝置引數資訊 collectDeviceInfo(mContext); //儲存日誌檔案 saveCrashInfo2File(ex); return true; } /** * 收集裝置引數資訊 * @param ctx */ 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); } } } /** * 儲存錯誤資訊到檔案中 * * @param ex * @return 返回檔名稱,便於將檔案傳送到伺服器 */ 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; } } |
在收集異常資訊時,朋友們也可以使用Properties,因為Properties有一個很便捷的方法properties.store(OutputStream out, String comments),用來將Properties例項中的鍵值對外輸到輸出流中,但是在使用的過程中發現生成的檔案中異常資訊列印在同一行,看起來極為費勁,所以換成Map來存放這些資訊,然後生成檔案時稍加了些操作。
完成這個CrashHandler後,我們需要在一個Application環境中讓其執行,為此,我們繼承android.app.Application,新增自己的程式碼,CrashApplication.java程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.scott.crash; import android.app.Application; public class CrashApplication extends Application { @Override public void onCreate() { super.onCreate(); CrashHandler crashHandler = CrashHandler.getInstance(); crashHandler.init(getApplicationContext()); } } |
最後,為了讓我們的CrashApplication取代android.app.Application的地位,在我們的程式碼中生效,我們需要修改AndroidManifest.xml:
1 2 |
<application android:name=".CrashApplication" ...> </application> |
因為我們上面的CrashHandler中,遇到異常後要儲存裝置引數和具體異常資訊到SDCARD,所以我們需要在AndroidManifest.xml中加入讀寫SDCARD許可權:
1 |
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> |
搞定了上邊的步驟之後,我們來執行一下這個專案:
看以看到,並不會有強制關閉的對話方塊出現了,取而代之的是我們比較有好的提示資訊。
然後看一下SDCARD生成的檔案:
用文字編輯器開啟日誌檔案,看一段日誌資訊:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 |
這些資訊對於開發者來說幫助極大,所以我們需要將此日誌檔案上傳到伺服器,有關檔案上傳的技術,請參照Android中使用HTTP服務相關介紹。
不過在使用HTTP服務之前,需要確定網路暢通,我們可以使用下面的方式判斷網路是否可用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * 網路是否可用 * * @param context * @return */ public static boolean isNetworkAvailable(Context context) { ConnectivityManager mgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo[] info = mgr.getAllNetworkInfo(); if (info != null) { for (int i = 0; i < info.length; i++) { if (info[i].getState() == NetworkInfo.State.CONNECTED) { return true; } } } return false; } |