上週工做中新來的小夥伴問了一下項目中CrashHandler,當時只是簡單講了一下
週末到了,心血來潮,手把手擼一個好用全面的CrashHandler吧,對於之後項目開發和當前項目的完善也有必定的幫助。javascript
CrashHandler
: 崩潰處理器,捕獲Crash信息並做出相應的處理java
CrashHandler
來將Crash寫入到本地方便咱們隨時隨地查看。CrashHandler
來幫咱們將崩潰信息返回給後臺,以便及時修復。下面咱們就手把手寫一個實用、本地化、輕量級的CrashHandler吧。git
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);
}複製代碼
/** * 獲取捕獲異常的信息 * * @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();
}
}複製代碼
/** * 從系統屬性中提取設備硬件和版本信息 */
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();
}
}
}複製代碼
/** * 獲取系統常規設定屬性 */
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);
}複製代碼
/** * 將崩潰日誌信息寫入本地文件 */
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;
}複製代碼
/** * 上傳崩潰信息到服務器 */
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);
會致使應用自動重啓三次,會影響一點用戶體驗github
因此咱們使用了一個土方法,就是讓它去打開一個咱們本身設定的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整個寫下來思路是三步 :
一、異常捕獲
二、信息數據採集
三、 數據寫入本地和上傳服務器
項目地址:Github地址
這是我一個隨便寫寫的項目
CrashHandler主要在rbase的util,還有app的MyApplication 中應用到服務器