未處理異常處理器 UncaughtExceptionHandler 實現 崩潰日誌儲存 與 重啟應用
前言
當我們編寫程式的時候 , 遇到會丟擲異常的方法的時候 , 我們一般會採取 try … catch 的方式:
try {
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
但是萬一我們捕捉不到的時候 , 程式就會FC , 彈出 某某某應用程式已停止執行 的對話方塊. 然後使用者在受到打擊之後還要重新點開應用 這從兩點來說都是很不好的 , 也是這就我們這次要實現的功能
- 當APP在使用者使用的時候 , 我們無法處理得到的異常
- 應用在崩潰之後沒有響應的處理
說個題外的小技巧
手動獲取日誌的方法 (需要ROOT許可權)
/data/system/dropbox 下存有所有應用的異常日誌檔案 , 非常時候可以手動提取
生成的日誌檔案:
崩潰後的提示:
重啟後顯示上次的異常日誌:
開始編寫異常處理器
要使用到的一些引數
/**
* Created by OCWVAR
* Package: com.ocwvar.surfacetest.ExceptionHandler
* Date: 2016/5/25 9:20
* Project: SurfaceTest
* 未處理異常接收器
*
* 在重新啟動Activity時會傳遞資料包 Bundle
* 也可直接使用 OCExceptionHandler.handleIncomingBundle() 方法進行處理
*
* 資料:
* IsRecover .布林型別. 區別這個資料是否為崩潰重啟的資料 永遠為 true
* hasLogs .布林型別. 是否成功生成了日誌檔案
* Throwable .Serializable序列化型別. 上次崩潰的異常物件
*
*
* 引數:
* SLEEPTIME_RESTART_ACTIVITY 重新啟動應用程式指定Activity間隔時間. 毫秒. 1000ms = 1s
* RESTART_ACTIVITY 重新啟動的Activity類
* LOG_NAME_HEAD 日誌檔名開頭
* LOG_SAVE_FOLDER 日誌儲存目錄
* SAVE_LOGS 是否生成日誌
*/
public class OCExceptionHandler{
private final static long SLEEPTIME_RESTART_ACTIVITY = 2000;
private final static Class RESTART_ACTIVITY = MainActivity.class;
private final static String LOG_NAME_HEAD = "OCLog";
private final static String LOG_SAVE_FOLDER = "/log/";
private final static boolean SAVE_LOGS = true;
public final static String THROWABLE_OBJECT = "Throwable";
public final static String IS_RECOVERY = "IsRecover";
public final static String HAS_LOGS = "hasLogs";
private boolean logsCreated = false;
...
}
1.引用 Thread.UncaughtExceptionHandler 介面 繼承 Application 類
(繼承Application只是為了設定 setDefaultUncaughtExceptionHandler , 和使用 ApplicationContext 而已 , 如果可以有其他實現方式可以不繼承)
這是就是我們一開始最基礎的配置
public class OCExceptionHandler extends Application implements Thread.UncaughtExceptionHandler {
@Override
public void onCreate() {
super.onCreate();
//設定捕捉全域性未處理異常為我們這個類
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread thread, Throwable ex){
...
}
2.在程式崩潰之後 , 啟動對應的Activity
/**
* 重新啟動應用程式
* @param activityClass 要啟動的Activity
*/
private void restartActivity(Class activityClass , Throwable throwable){
//建立用於啟動的 Intent , 與對應的資料
Intent intent = new Intent(getApplicationContext(),activityClass);
intent.putExtra("IsRecover",true);
intent.putExtra("hasLogs",logsCreated);
intent.putExtra("Throwable",throwable);
PendingIntent pendingIntent = PendingIntent.getActivity(
getApplicationContext(),
0,
intent,
PendingIntent.FLAG_ONE_SHOT
);
//獲取鬧鐘管理器 , 用於定時執行我們的啟動任務
AlarmManager mgr = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
//設定執行PendingIntent的時間是當前時間+SLEEPTIME_RESTART_ACTIVITY 引數的值
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + SLEEPTIME_RESTART_ACTIVITY , pendingIntent);
}
3.在程式崩潰的時候記錄下日誌檔案
/**
* 建立日誌檔案
* @param throwable 要記錄的異常
* @return 執行結果
*/
private boolean createLogs(Throwable throwable){
if (throwable == null || !SAVE_LOGS){
return false;
}
if (Build.VERSION.SDK_INT >= 23 && !checkPermission()){
Log.e("異常處理--儲存日誌", "儲存失敗 , Android 6.0+ 系統. 記憶體卡讀寫許可權沒有獲取" );
return false;
}
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
//如果記憶體卡或內建儲存已經掛載
//建立儲存目錄
String savePath = Environment.getExternalStorageDirectory().getPath()+LOG_SAVE_FOLDER;
File file = new File(savePath);
file.mkdirs();
if (file.canWrite()){
//如果目錄可以寫入
FileWriter fileWriter;
PrintWriter printWriter;
//得到當前的日期與時間 , 精確到秒
String exceptionTime = DateFormat.format("yyyy-MM-dd hh:mm:ss", new Date()).toString();
//建立日誌檔案物件
file = new File(savePath + LOG_NAME_HEAD + " " + exceptionTime + " .log");
try {
if (file.createNewFile()){
//如果檔案建立成功 , 則寫入檔案
fileWriter = new FileWriter(file,true);
printWriter = new PrintWriter(fileWriter);
printWriter.println("Date:"+exceptionTime+"\n");
printWriter.println("Exception Class Name: ");
printWriter.println(throwable.getStackTrace()[0].getClassName());
printWriter.println("");
printWriter.println("Exception Class Position: ");
printWriter.println("Line number: "+throwable.getStackTrace()[0].getLineNumber());
printWriter.println("");
printWriter.println("Exception Cause: ");
printWriter.println(throwable.getMessage());
printWriter.println("");
printWriter.println("-----------------------------------\nException Message: \n");
for (int i = 0; i < throwable.getStackTrace().length; i++) {
printWriter.println(throwable.getStackTrace()[i]);
}
//清空與關閉用到的流
printWriter.flush();
fileWriter.flush();
printWriter.close();
fileWriter.close();
Log.w("異常處理--儲存日誌", "日誌儲存成功" );
return true;
}else {
Log.e("異常處理--儲存日誌", "儲存失敗 , 存在相同名稱的日誌檔案" );
return false;
}
} catch (IOException e) {
Log.e("異常處理--儲存日誌", "儲存失敗 , 無法建立日誌檔案或寫入流失敗" );
return false;
}
}else {
//目錄不可寫入 , 操作失敗
Log.e("異常處理--儲存日誌", "儲存失敗 , 無法寫入目錄" );
file = null;
return false;
}
}else {
Log.e("異常處理--儲存日誌", "儲存失敗 , 儲存未掛載" );
return false;
}
}
/**
* 檢查記憶體卡讀寫許可權 針對Android 6.0+
* @return 是否有許可權
*/
@TargetApi(23)
private boolean checkPermission(){
return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
4.結尾工作 , 使用我們建立的方法
在方法public void uncaughtException(Thread thread, Throwable ex)中呼叫我們的方法
@Override
public void uncaughtException(Thread thread, Throwable ex) {
//記錄日誌生成結果 , 用於給Intent傳遞日誌生存的結果
logsCreated = createLogs(ex);
restartActivity(RESTART_ACTIVITY , ex);
//應用已經崩潰, 需要先終止當前的應用執行緒. 否則會ANR
System.exit(2);
}
然後在配置檔案中使用我們這個類 (如果你是用你自己的Application類就不需要了):
<application
android:icon="@mipmap/ic_launcher"
android:label="TEST TEST TEST"
android:theme="@style/AppTheme"
//在這裡進行註冊Application類
android:name=".ExceptionHandler.OCExceptionHandler">
...
</application>
注意
有的大佬可能會想在程式崩潰的時候顯示一個Toast或對話方塊 , 但這是不行的 , 無論你是直接執行還是用Handler .我看了下Stack Overflow上的QA , 有個人提出的觀點 , 也是我最認同的觀點 :
當應用崩潰的時候 , 已經沒有能用的ApplicationContext了 ,所以你用了之後都是沒反應的.
但我發現如果你用的是Activity.Context就能達到目的 , 但是這會導致Activity無法被回收的風險 , 所以非常不建議這麼用. 畢竟為了顯示一句話而導致記憶體洩漏這就撿了芝麻丟了西瓜.
彌補缺點
我們既然不方便在 UncaughtExceptionHandler 裡面進行提示 , 但我們既然會傳 Intent 給啟動的Activity , 那麼我們直接用它來做就行了.
/**
* 處理上次崩潰重啟傳回Activity的Bundle資料
* @param bundle 傳入的Bundle資料
* @return True: 處理成功 False:不是上次崩潰時傳入的資料
*/
public static boolean handleIncomingBundle(@NonNull Bundle bundle , Context context){
//判斷這個Bundle是不是我們崩潰後傳回的資料
if (bundle.get(IS_RECOVERY) != null){
if (bundle.getBoolean(HAS_LOGS)){
Toast.makeText(context, "程式已恢復 , 崩潰日誌已生成", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(context, "程式已恢復", Toast.LENGTH_SHORT).show();
}
Throwable throwable = (Throwable)bundle.getSerializable(THROWABLE_OBJECT);
if (throwable != null){
Log.e("上次崩潰日誌", "---------------------------------------------");
for (int i = 0; i < throwable.getStackTrace().length; i++) {
System.out.println(throwable.getStackTrace()[i]);
}
Log.e("上次崩潰日誌", "---------------------------------------------");
}else {
Log.e("上次崩潰日誌", "日誌丟失 或 記錄失敗 !");
}
return true;
}else {
return false;
}
}
在要啟動的Activity中進行使用即可:
if (getIntent().getExtras() != null){
OCExceptionHandler.handleIncomingBundle(getIntent().getExtras(),getApplicationContext());
}