擼一個專案必備的CrashHandler
上週工作中新來的小夥伴問了一下專案中CrashHandler,當時只是簡單講了一下
週末到了,心血來潮,手把手擼一個好用全面的CrashHandler吧,對於以後專案開發和當前專案的完善也有一定的幫助。
目錄
- 認識與作用
- Crash的捕獲
- Crash資訊的獲取
- Crash日誌寫入上傳
- 使用方式
- 一些注意
- 最後
認識與作用
CrashHandler
: 崩潰處理器,捕獲Crash資訊並作出相應的處理
- 測試使用:應用在日常的開發中,我們經常需要去Logcat測試我們的App,但由於很多原因,Android Monitor會閃屏或者Crash資訊丟失。 這個時候就需要一個
CrashHandler
來將Crash寫入到本地方便我們隨時隨地檢視。 - 上線使用:應用的崩潰率是使用者衡量篩選應用的重要標準,那麼應用上線以後 我們無法向使用者藉手機來分析崩潰原因。為了減低崩潰率,這個時候需要
CrashHandler
來幫我們將崩潰資訊返回給後臺,以便及時修復。
下面我們就手把手寫一個實用、本地化、輕量級的CrashHandler吧。
Crash的捕獲
- 實現
Thread.UncaughtExceptionHandler
介面,並重寫uncaughtException
方法,此時你的CrashHandler就具備了接收處理異常的能力了。 - 呼叫
Thread.setDefaultUncaughtExceptionHandler(CrashHandler)
,來使用我們自定義的CrashHandler
來取代系統預設的CrashHandler
- 結合單例模式
- 總體三步: 捕獲異常、資訊資料獲取、資料寫入和上傳
總體的初始化程式碼如下:
private RCrashHandler(String dirPath) {
mDirPath = dirPath;
File mDirectory = new File(mDirPath);
if (!mDirectory.exists()) {
mDirectory.mkdirs();
}
}
public static RCrashHandler getInstance(String dirPath) {
if (INSTANCE == null) {
synchronized (RCrashHandler.class) {
if (INSTANCE == null) {
INSTANCE = new RCrashHandler(dirPath);
}
}
}
return INSTANCE;
}
/**
* 初始化
*
* @param context 上下文
* @param crashUploader 崩潰資訊上傳介面回撥
*/
public void init(Context context, CrashUploader crashUploader) {
mCrashUploader = crashUploader;
mContext = context;
//儲存一份系統預設的CrashHandler
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//使用我們自定義的異常處理器替換程式預設的
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 這個是最關鍵的函式,當程式中有未被捕獲的異常,系統將會自動呼叫uncaughtException方法
*
* @param t 出現未捕獲異常的執行緒
* @param e 未捕獲的異常,有了這個ex,我們就可以得到異常資訊
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
if (!catchCrashException(e) && mDefaultHandler != null) {
//沒有自定義的CrashHandler的時候就呼叫系統預設的異常處理方式
mDefaultHandler.uncaughtException(t, e);
} else {
//退出應用
killProcess();
}
}
/**
* 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成.
*
* @param ex
* @return true:如果處理了該異常資訊;否則返回false.
*/
private boolean catchCrashException(Throwable ex) {
if (ex == null) {
return false;
}
new Thread() {
public void run() {
// Looper.prepare();
// Toast.makeText(mContext, "很抱歉,程式出現異常,即將退出", 0).show();
// Looper.loop();
Intent intent = new Intent();
intent.setClass(mContext, CrashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityCollector.finishAll();
mContext.startActivity(intent);
}
}.start();
//收集裝置引數資訊
collectInfos(mContext, ex);
//儲存日誌檔案
saveCrashInfo2File();
//上傳崩潰資訊
uploadCrashMessage(infos);
return true;
}
/**
* 退出應用
*/
public static void killProcess() {
//結束應用
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
ToastUtils.showLong("哎呀,程式發生異常啦...");
Looper.loop();
}
}).start();
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
RLog.e("CrashHandler.InterruptedException--->" + ex.toString());
}
//退出程式
Process.killProcess(Process.myPid());
System.exit(1);
}複製程式碼
Crash資訊的獲取
- 獲取異常資訊
/**
* 獲取捕獲異常的資訊
*
* @param ex
*/
private String collectExceptionInfos(Throwable ex) {
Writer mWriter = new StringWriter();
PrintWriter mPrintWriter = new PrintWriter(mWriter);
ex.printStackTrace(mPrintWriter);
ex.printStackTrace();
Throwable mThrowable = ex.getCause();
// 迭代棧佇列把所有的異常資訊寫入writer中
while (mThrowable != null) {
mThrowable.printStackTrace(mPrintWriter);
// 換行 每個個異常棧之間換行
mPrintWriter.append("\r\n");
mThrowable = mThrowable.getCause();
}
// 記得關閉
mPrintWriter.close();
return mWriter.toString();
}複製程式碼
- 獲取應用資訊
/**
* 獲取應用包引數資訊
*/
private void collectPackageInfos(Context context) {
try {
// 獲得包管理器
PackageManager mPackageManager = context.getPackageManager();
// 得到該應用的資訊,即主Activity
PackageInfo mPackageInfo = mPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (mPackageInfo != null) {
String versionName = mPackageInfo.versionName == null ? "null" : mPackageInfo.versionName;
String versionCode = mPackageInfo.versionCode + "";
mPackageInfos.put(VERSION_NAME, versionName);
mPackageInfos.put(VERSION_CODE, versionCode);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}複製程式碼
- 獲取裝置硬體資訊(針對不同機型的使用者更有效地定位Bug)
/**
* 從系統屬性中提取裝置硬體和版本資訊
*/
private void collectBuildInfos() {
// 反射機制
Field[] mFields = Build.class.getDeclaredFields();
// 迭代Build的欄位key-value 此處的資訊主要是為了在伺服器端手機各種版本手機報錯的原因
for (Field field : mFields) {
try {
field.setAccessible(true);
mDeviceInfos.put(field.getName(), field.get("").toString());
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}複製程式碼
- 獲取系統常規資訊(針對不同設定的使用者更有效定位Bug)
/**
* 獲取系統常規設定屬性
*/
private void collectSystemInfos() {
Field[] fields = Settings.System.class.getFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Deprecated.class)
&& field.getType() == String.class) {
try {
String value = Settings.System.getString(mContext.getContentResolver(), (String) field.get(null));
if (value != null) {
mSystemInfos.put(field.getName(), value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}複製程式碼
- 獲取安全設定資訊
/**
* 獲取系統安全設定資訊
*/
private void collectSecureInfos() {
Field[] fields = Settings.Secure.class.getFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Deprecated.class)
&& field.getType() == String.class
&& field.getName().startsWith("WIFI_AP")) {
try {
String value = Settings.Secure.getString(mContext.getContentResolver(), (String) field.get(null));
if (value != null) {
mSecureInfos.put(field.getName(), value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}複製程式碼
- 獲取應用記憶體資訊(需要許可權)
/**
* 獲取記憶體資訊
*/
private String collectMemInfos() {
BufferedReader br = null;
StringBuffer sb = new StringBuffer();
ArrayList<String> commandLine = new ArrayList<>();
commandLine.add("dumpsys");
commandLine.add("meminfo");
commandLine.add(Integer.toString(Process.myPid()));
try {
java.lang.Process process = Runtime.getRuntime()
.exec(commandLine.toArray(new String[commandLine.size()]));
br = new BufferedReader(new InputStreamReader(process.getInputStream()), 8192);
while (true) {
String line = br.readLine();
if (line == null) {
break;
}
sb.append(line);
sb.append("\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}複製程式碼
- 最後將這些資訊儲存到
infos
中,以便之後我們回傳給上傳具體功能時候更加方便,有了這些資料,我們應該能夠快速定位崩潰的原因了
/**
* 獲取裝置引數資訊
*
* @param context
*/
private void collectInfos(Context context, Throwable ex) {
mExceptionInfos = collectExceptionInfos(ex);
collectPackageInfos(context);
collectBuildInfos();
collectSystemInfos();
collectSecureInfos();
mMemInfos = collectMemInfos();
//將資訊儲存到一個總的Map中提供給上傳動作回撥
infos.put(EXCEPETION_INFOS_STRING, mExceptionInfos);
infos.put(PACKAGE_INFOS_MAP, mPackageInfos);
infos.put(BUILD_INFOS_MAP, mDeviceInfos);
infos.put(SYSTEM_INFOS_MAP, mSystemInfos);
infos.put(SECURE_INFOS_MAP, mSecureInfos);
infos.put(MEMORY_INFOS_STRING, mMemInfos);
}複製程式碼
Crash日誌寫入
- 將崩潰資料寫入到本地檔案中(這裡我只收集了異常資訊 和 應用資訊,具體情況可以根據自己需求來拼接其他資料)
/**
* 將崩潰日誌資訊寫入本地檔案
*/
private String saveCrashInfo2File() {
StringBuffer mStringBuffer = getInfosStr(mPackageInfos);
mStringBuffer.append(mExceptionInfos);
// 儲存檔案,設定檔名
String mTime = formatter.format(new Date());
String mFileName = "CrashLog-" + mTime + ".log";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
try {
File mDirectory = new File(mDirPath);
Log.v(TAG, mDirectory.toString());
if (!mDirectory.exists())
mDirectory.mkdirs();
FileOutputStream mFileOutputStream = new FileOutputStream(mDirectory + File.separator + mFileName);
mFileOutputStream.write(mStringBuffer.toString().getBytes());
mFileOutputStream.close();
return mFileName;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 將HashMap遍歷轉換成StringBuffer
*/
@NonNull
public static StringBuffer getInfosStr(ConcurrentHashMap<String, String> infos) {
StringBuffer mStringBuffer = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
mStringBuffer.append(key + "=" + value + "\r\n");
}
return mStringBuffer;
}複製程式碼
- 由於每一個應用上傳的伺服器或者資料型別都不同,所以為了更好的延展性,我們使用介面回撥,將獲取的全部資料拋給應用具體去實現(我這裡為了演示,使用了Bmob後端雲)
/**
* 上傳崩潰資訊到伺服器
*/
public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
mCrashUploader.uploadCrashMessage(infos);
}
/**
* 崩潰資訊上傳介面回撥
*/
public interface CrashUploader {
void uploadCrashMessage(ConcurrentHashMap<String, Object> infos);
}複製程式碼
使用方式
/**
* 初始化崩潰處理器
*/
private void initCrashHandler() {
mCrashUploader = new RCrashHandler.CrashUploader() {
@Override
public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
CrashMessage cm = new CrashMessage();
ConcurrentHashMap<String, String> packageInfos = (ConcurrentHashMap<String, String>) infos.get(RCrashHandler.PACKAGE_INFOS_MAP);
cm.setDate(DateTimeUitl.getCurrentWithFormate(DateTimeUitl.sysDateFormate));
cm.setVersionName(packageInfos.get(RCrashHandler.VERSION_NAME));
cm.setVersionCode(packageInfos.get(RCrashHandler.VERSION_CODE));
cm.setExceptionInfos(((String) infos.get(RCrashHandler.EXCEPETION_INFOS_STRING)));
cm.setMemoryInfos((String) infos.get(RCrashHandler.MEMORY_INFOS_STRING));
cm.setDeviceInfos(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
.get(RCrashHandler.BUILD_INFOS_MAP)).toString());
cm.setSystemInfoss(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
.get(RCrashHandler.SYSTEM_INFOS_MAP)).toString());
cm.setSecureInfos(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
.get(RCrashHandler.SECURE_INFOS_MAP)).toString());
cm.save(new SaveListener<String>() {
@Override
public void done(String s, BmobException e) {
if (e == null) {
RLog.e("上傳成功!");
} else {
RLog.e("上傳Bmob失敗 錯誤碼:" + e.getErrorCode());
}
}
});
}
};
RCrashHandler.getInstance(FileUtils.getRootFilePath() + "EasySport/crashLog")
.init(mAppContext, mCrashUploader);
}複製程式碼
一些注意
使用過程中發現在Activity中 Process.killProcess(Process.myPid());
和System.exit(1);
會導致應用自動重啟三次,會影響一點使用者體驗
所以我們使用了一個土方法,就是讓它去開啟一個我們自己設定的CrashActivity
來提高我們應用的使用者體驗
/**
* 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成.
*
* @param ex
* @return true:如果處理了該異常資訊;否則返回false.
*/
private boolean catchCrashException(Throwable ex) {
if (ex == null) {
return false;
}
//啟動我們自定義的頁面
new Thread() {
public void run() {
// Looper.prepare();
// Toast.makeText(mContext, "很抱歉,程式出現異常,即將退出", 0).show();
// Looper.loop();
Intent intent = new Intent();
intent.setClass(mContext, CrashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityCollector.finishAll();
mContext.startActivity(intent);
}
}.start();
//收集裝置引數資訊
collectInfos(mContext, ex);
//儲存日誌檔案
saveCrashInfo2File();
//上傳崩潰資訊
uploadCrashMessage(infos);
return true;
}複製程式碼
CrashActivity
的話就看個人需求了,可以使一段Sorry的文字或者一些互動的反饋操作都是可以的。
最後
CrashHandler整個寫下來思路是三步 :
1、異常捕獲
2、資訊資料採集
3、 資料寫入本地和上傳伺服器
專案地址:Github地址
這是我一個隨便寫寫的專案
CrashHandler主要在rbase的util,還有app的MyApplication 中應用到