1. 程式人生 > >Android中全域性異常捕獲以及動態logcat列印。方便上線專案分析

Android中全域性異常捕獲以及動態logcat列印。方便上線專案分析

很多時候我們會出現出現了一個問題,但是我們自己並沒有日誌的情況。這個時候怎麼辦呢。其實在我們的軟體中整合一些日誌上報的功能有時候是有需要的。那麼問題來了:我們該在自己程式碼中動態捕獲自己應用的日誌,以及錯誤資訊呢。其實android 給出了兩種:
1.執行時異常捕獲:
這個很容易明白,就是在程式正常執行中,如果程式出現了全域性的異常,那麼我們就捕獲異常,並且把異常資訊給收集處理。比如我們可以通過指定的後臺介面進行上傳,這樣就方便我們後期對這些異常的處理。
那我們最關心的是如何處理呢:先上程式碼。

public class CrashHandler implements Thread
.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"); private String nameString="/logcat"; /** 保證只有一個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; } // WonderMapApplication.getInstance().getSpUtil().setCrashLog(true);// 每次進入應用檢查,是否有log,有則上傳 // 使用Toast來顯示異常資訊 new Thread() { @Override public void run() { Looper.prepare(); Toast.makeText(mContext, "很抱歉,程式出現異常,正在收集日誌,即將退出", Toast.LENGTH_LONG) .show(); Looper.loop(); } }.start(); // 收集裝置引數資訊 collectDeviceInfo(mContext); // 儲存日誌檔案 //getLog(); String fileName = 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 (PackageManager.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(); Log.d("########", result); sb.append(result); try { long timestamp = System.currentTimeMillis(); String time = formatter.format(new Date()); String fileName = nameString + "-" + time + "-" + timestamp + ".log"; if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { Log.e("tags",Environment.getExternalStorageDirectory().getAbsolutePath()); String path = Environment.getExternalStorageDirectory().getAbsolutePath()+LogFileDivider.LOG_FILE; 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; } }

程式碼中有明確的程式碼註釋這邊就不過多的解釋了。但是主要的核心呢其實就是實現的UncaughtExceptionHandler這個介面。它裡面需要重寫一個uncaughtException方法,這個方法就是全域性異常出現的回撥。我們只需要重寫它就能拿到其中的錯誤資訊。看下原始碼:

 public static interface UncaughtExceptionHandler {
        /**
         * The thread is being terminated by an uncaught exception. Further
         * exceptions thrown in this method are prevent the remainder of the
         * method from executing, but are otherwise ignored.
         *
         * @param thread the thread that has an uncaught exception
         * @param ex the exception that was thrown
         */
        void uncaughtException(Thread thread, Throwable ex);
    }

這樣我們可以通過自己的處理加上一些類似裝置資訊呀。然後獲取異常中的message 這樣異常錯誤我們就能找到了。很方便上線的錯誤定位。但是這個異常只針對執行時的,像一些anr問題就沒有任何提示,這樣就拿不到我們需要的錯誤資訊了。並且其中只包含了異常資訊。並沒有我們想要的日誌。

2.執行logcat動態捕獲:
這個效果就能做到我們開發中ide中的logcat異常的效果。其實也是幫助非常大的,因為我們有許多時候是想知道這些日誌的。那我們該如何實現呢。
android 許可權機制中有一個許可權叫:

<uses-permission android:name="android.permission.READ_LOGS" />

這個是讀取log的許可權。但是我們要怎麼讀呢。其實就是通過Runtime.getRuntime().exec(command);通過獲得執行時環境(也就是java虛擬機器)來進入到對應的平臺。比如安卓中會到底層的linux系統中,通過shell執行一些command命令。其實我們平時經常會通過adbshell 來除錯我們的手機,和這個的原理應該是一樣的。在adb shell 中我們會通過adb logcat 來檢視android裝置上的日誌。

adb logcat -s System.out   輸出System.out標籤的資訊
adb logcat -f /sdcard/log.txt   輸出到手機檔案上,可以使用&後臺執行
adb shell ps | grep logcat     查詢程序
adb shell ps | findstr "logcat" 查詢程序,windows平臺命令

adb logcat > log
ctrl C
more log    使用more log 檢視日誌資訊

adb logcat -d -v /sdcard/mylog.txt  日誌儲存在手機指定位置
adb logcat -v time   檢視日誌輸出時間
adb logcat -v threadtime   日誌輸出時間和執行緒資訊
adb logcat -v brief        預設的日誌格式
adb logcat -v process      程序優先順序日誌資訊
adb logcat -v tag           標籤優先順序日誌資訊
adb logcat -v raw          只輸出日誌不輸出其它資訊
adb logcat -v time         time格式輸出

adb logcat -c               清空
adb logcat -d             快取輸出
adb logcat -t 5          最近5行
adb logcat -g           檢視日誌緩衝區
adb logcat -b         載入日誌緩衝區
adb logcat -B         二進位制形式輸出日誌

adb logcat  *:E    顯示Error以上級別日誌

一系列的指令的都是可以檢視日誌的。還可以通過規則進行過濾。
那麼安卓中的指令是什麼呢:

//列印V級別以上的log包含時間
String cmd="logcat -d -v time -t 1000 *:V"  
Runtime.getRuntime().exec(cmd)

這樣我們可以對 返回的Process 進行讀取

 Process process=Runtime.getRuntime().exec(cmd);
 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
 String LogStr="";
 String line;
 while ((line = bufferedReader.readLine()) != null) {
   //清理日誌,如果你這裡做了sout,那麼你輸出的內容也會被記錄,就會出現問題
   LogStr=line+"\n"+LogStr;
  }

這樣我們就能夠拿到讀取的log 了。 其實和logcat裡面的很像,但是會有一些區別。
有了log我們就可以做對應的處理了。也是非常簡單。檔案儲存呀,伺服器上傳呀等等。
這個logcat github上有個很好的開源專案,有需要可以看下,做了一些封裝。地址:
https://github.com/tianzhijiexian/Logcat.git
這樣呢兩種日誌和異常捕獲我們都說完了。其實因為第二種捕獲logcat 無法實時捕獲,到底什麼時候捕獲呢。這個可以根據自己的需求來定,當然你可以和第一種結合起來使用,就是出現異常的時候我們先捕獲全域性的log,再做其他的異常處理、這樣也能更加有助我們進行分析。