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,再做其他的異常處理、這樣也能更加有助我們進行分析。