導讀
最近公司有一個需求,就是如何讓App 不奔潰或者奔潰后可以自動重啟?咋一聽,可能你和我都會說,對可能Crash的地方try…catch 不就可以了?
然而細琢磨一下這個問題,其實并非如此簡單,,,,接下來大家就跟我一起看看App Crash背后的緣由吧!
問題細化
如何讓自己的App不奔潰呢?其實問題主要涉及一下幾個點:
1、App為什么會Crash?
2、未捕獲到的例外導致的Crash怎么辦?
3、有什么辦法可以讓APP不奔潰呢?
4、假如App奔潰后,能否自動重啟呢?
帶著這幾個問題,和大家分享一下我查閱到的解決方案,以下內容或多或少參閱了其他博文,請大家不要見笑,,就當是自己學習總結了下哈,
探究一:App為什么會Crash?
首先捕獲程式崩潰的例外就必須了解一下java中UncaughtExceptionHandler這個介面,android沿用了此介面,在android API中通過實作此介面,能夠處理執行緒被一個無法捕捉的例外所終止的情況,
在java API中對該介面描述如下:
在實作UncaughtExceptionHandler時,必須多載uncaughtException(Thread thread, Throwable ex)
1、如果我們沒有實作該介面,也就是沒有顯示捕捉例外,則ex為空,否則ex不為空,thread 則為出例外的執行緒;
2、如果想捕獲例外我們可以實作這個介面或者繼承ThreadGroup,并多載uncaughtException方法,
顯示處理執行緒例外終止的情況;
一、首先我們看下執行緒中拋出例外以后的處理邏輯,一旦代碼拋出例外,并且我們沒有捕捉的情況下,JVM 會呼叫 Thread 的 dispatchUncaughtException 方法
public final void dispatchUncaughtException(Throwable e) {
Thread.UncaughtExceptionHandler initialUeh =
Thread.getUncaughtExceptionPreHandler();
if (initialUeh != null) {
try {
initialUeh.uncaughtException(this, e);
} catch (RuntimeException | Error ignored) {
// Throwables thrown by the initial handler are ignored
}
}
//這里會獲取對應的 UncaughtExceptionHandler 物件,然后呼叫對應的 uncaughtException 方法
getUncaughtExceptionHandler().uncaughtException(this, e);
}
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
//可以看到當 uncaughtExceptionHandler 沒有賦值的時候,會回傳 ThreadGroup 物件
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
通過原始碼得知,如果 App 中沒有手動設定 uncaughtExceptionHandler 物件,那么會執行 ThreadGroup的uncaughtException 方法:
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
然后呼叫 Thread.getDefaultUncaughtExceptionHandler() 獲取默認的 UncaughtExceptionHandler ,然后呼叫 uncaughtException 方法,既然名字是默認的 uncaughtExceptionHandler 物件,那么必然有初始化的地方…這就需要從系統初始化開始說起,為了簡化初始化流程,咱們直接從 RuntimeInit 的 main 方法開始說起
@UnsupportedAppUsage
public static final void main(String[] argv) {
enableDdms();
if (argv.length == 2 && argv[1].equals("application")) {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application");
redirectLogStreams();
} else {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting tool");
}
commonInit();
/*
* Now that we're running in interpreted code, call back into native code
* to run the system.
*/
nativeFinishInit();
if (DEBUG) Slog.d(TAG, "Leaving RuntimeInit!");
}
@UnsupportedAppUsage
protected static final void commonInit() {
if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
/*
* set handlers; these apply to all threads in the VM. Apps can replace
* the default handler, but not the pre handler.
*/
LoggingHandler loggingHandler = new LoggingHandler();
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler))
...代碼省略...
initialized = true;
}
在這里,給Thread 設定一個 KillApplicationHandler 物件,而 KillApplicationHandler實作了 Thread.UncaughtExceptionHandler 這個介面,那么必然會重寫 uncaughtException 方法,
App為什么會Crash?關鍵代碼就在這里
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
ensureLogging(t, e);
// Don't re-enter -- avoid infinite loops if crash-reporting crashes.
if (mCrashing) return;
mCrashing = true;
// Try to end profiling. If a profiler is running at this point, and we kill the
// process (below), the in-memory buffer will be lost. So try to stop, which will
// flush the buffer. (This makes method trace profiling useful to debug crashes.)
if (ActivityThread.currentActivityThread() != null) {
ActivityThread.currentActivityThread().stopProfiling();
}
// Bring up crash dialog, wait for it to be dismissed
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
if (t2 instanceof DeadObjectException) {
// System process is dead; ignore
} else {
try {
Clog_e(TAG, "Error reporting crash", t2);
} catch (Throwable t3) {
// Even Clog_e() fails! Oh well.
}
}
} finally {
// Try everything to make sure this process goes away.
Process.killProcess(Process.myPid());
System.exit(10);
}
}
代碼最后執行了 System.exit(10) ;而這個方法會直接干掉當前行程,也就是所謂的 App crash 了,所以我們一旦拋出例外,并且沒有捕捉的話,程式就會被強制干掉,
探究二:未捕獲的例外導致的Crash怎么辦?
這個問題其實上面有提到,實作UncaughtExceptionHandler介面,顯示處理執行緒例外終止就行,具體方法如下:
/**
* Created by lxb on 2020/12/11
* <p>
* Thread.UncaughtExceptionHandler
* 介面說明:能夠處理執行緒被一個無法捕獲的例外所終止
*/
public class UnExcepHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "UnExcepHandler";
private CatchExceptionApp application;
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public UnExcepHandler(CatchExceptionApp application) {
// 獲取系統默認的UncaughtExceptionHandler 處理器
defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
this.application = application;
}
/**
* @param thread Thread
* @param ex ex ==null,表示沒有顯示的處理例外;
* ex!=null,表示 thread為出例外的執行緒
*/
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
// 如果用戶沒有處理,則讓系統默認的例外處理器處理
if (!handlerException(ex) && defaultUncaughtExceptionHandler != null) {
defaultUncaughtExceptionHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Intent intent = new Intent(application, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NEW_TASK);
@SuppressLint("WrongConstant") PendingIntent restartIntent =
PendingIntent.getActivity(application.getApplicationContext(), 0,
intent, intent.getFlags());
// 退出程式,1秒后自動重啟程式
AlarmManager alarmManager = (AlarmManager) application.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC,
System.currentTimeMillis() + 1000, restartIntent);
// application.finishAllActivity();
}
}
/**
* 自定義錯誤處理 / 收集錯誤資訊 / 發送錯誤報告 都在此處理
*
* @param ex
* @return true: 處理了該例外 false: 不處理例外(系統處理)
*/
public boolean handlerException(Throwable ex) {
if (ex == null) {
return false;
}
// 顯示無法捕獲的例外
new Thread() {
@Override
public void run() {
super.run();
Looper.prepare();
Toast.makeText(application.getApplicationContext(),
"很抱歉,程式出現例外,即將退出,", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
return true;
}
通過在android Application 這個全域類中處理例外即可,
/**
* Created by lxb on 2020/12/11
* 全域捕獲例外
*/
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public void init() {
// 設定UnExcepHandler 為程式的默認處理器
UnExcepHandler unExcepHandler = new UnExcepHandler(this);
Thread.setDefaultUncaughtExceptionHandler(unExcepHandler);
}
探究三:假如App奔潰后,能否自動重啟呢?
如何殺死例外行程,重啟應用,就得使用PendingIntent,這個類是android中對Intent類的包裝,具體代碼如下:
/**
* @param thread Thread
* @param ex ex ==null,表示沒有顯示的處理例外;
* ex!=null,表示 thread為出例外的執行緒
*/
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
// 如果用戶沒有處理,則讓系統默認的例外處理器處理
if (!handlerException(ex) && defaultUncaughtExceptionHandler != null) {
defaultUncaughtExceptionHandler.uncaughtException(thread, ex);
} else {
Intent intent = new Intent(application, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NEW_TASK);
@SuppressLint("WrongConstant") PendingIntent restartIntent =
PendingIntent.getActivity(application.getApplicationContext(), 0,
intent, intent.getFlags());
// 退出程式,1秒后自動重啟程式
AlarmManager alarmManager = (AlarmManager) application.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC,
System.currentTimeMillis() + 1000, restartIntent);
// application.finishAllActivity();
}
}
通過AlarmManager 啟動它,并且關閉打開的Activity殺死例外行程就能夠實作重新啟動應用,
探究四:有什么辦法可以讓APP不奔潰呢?
通過以上三個探究,很顯然是有辦法讓App 不奔潰的,再重新看下這段原始碼:
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
//可以看到當uncaughtExceptionHandler沒有賦值的時候,會回傳ThreadGroup物件
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
上面有提到只有在沒有手動設定 UncaughtExceptionHandler 的時候,才會呼叫
defaultUncaughtExceptionHandler 物件,所以自然而然的就想到了實作這個類,然后在這里面做相應的處理,
接下來咱們可以寫一個demo來驗證這個推測到底可行不可行,,,
1、首先人為制造一個例外(主動拋出例外)看看效果
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnCrashTwo: // 主執行緒拋出例外不奔潰處理
mainThreadCrash();
break;
}
}
private void mainThreadCrash() {
int i = 1 / 0;
Log.d("MainActivity", "i:" + i);
}
來看一下執行效果(運行效果圖后期補上,抱歉!):

不出意料程式崩潰了!!!
那接下來咱們手動設定一個UncaughtExceptionHandler
/**
* Created by lxb on 2020/12/11
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable e) {
Log.d(TAG, "[uncaughtException] : " + e.getMessage());
// 通過handler將toast拋到主執行緒彈出
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(CatchExceptionApp.mApp, "[uncaughtException] message:\n"
+ e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public static CatchExceptionApp mApp;
@Override
public void onCreate() {
super.onCreate();
mApp = this;
//主執行緒Crash處理
/**
* Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());
* tip:
* 這個是設定當前執行緒的方法,總不能給每個Thread 都設定一個,這肯定不可取
* Thread 中還有一個全域靜態的 UncaughtExceptionHandler 可以解決這一問題
*/
Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());
}
}
再運行看下效果(運行效果圖后期補上,抱歉!):

看來我們的推測是正確的,確實 App 已經不會 crash 了,但是又出現了另外一個問題, App 卡死在了這個界面,怎么點擊也沒反應了,
那么這到底是怎么一回事呢?其實這也不難理解,我們的頁面啟動的入口是在 ActivityThread 的 main 方法:在這里面進行初始化主執行緒的 Loop ,然后執行 loop 回圈,我們知道 Looper 是用來回圈遍歷訊息佇列的,一旦訊息佇列中存在訊息,那么就會執行里面的操作,
整個 Android 系統就是基于事件驅動的,而事件主要就是基于 Looper 來獲取的,所以如果這里一旦出現
crash,那么就直接會跳出整個 main 方法,自然 loop 回圈也就跳出了,那么自然而然事件也就接收不到,更沒法處理,所以整個 App
就會卡死在這里,
2、有沒有其他辦法可以保證 App 在拋出例外不 crash 的情況下,又能保證不會卡死呢?
既然 looper 是查詢事件的核心類,那么我們是否可以不讓跳出 loop 回圈呢,乍一想好像沒辦法做到,我們沒法給 loop 方法 try-catch ,但是我們可以給訊息佇列發送一個 loop 回圈,然后給這個 loop 做一個 try-catch ,一旦外層的 loop 檢測到這個事件,就會執行我們自己創建的 loop 回圈,這樣以后 App 內的所有事件都會在我們自己的 loop 回圈中處理,一旦拋出例外,跳出 loop 回圈以后,我們也可以在 loop 外層套一層 while 回圈,讓自己的 loop 再次作業,
再通過代碼驗證下咱們的推測吧:
package com.lxb.app_crash_auto_reboot;
import android.app.Activity;
import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
/**
* Created by lxb on 2020/12/11
* 全域捕獲例外
*/
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public static CatchExceptionApp mApp;
@Override
public void onCreate() {
super.onCreate();
mApp = this;
//主執行緒Crash處理
Handler handler = new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 未捕獲的例外導致APP Crash 后,通過while 讓自己創建的loop 再次作業
while (true) {
//給自己創建的loop 添加 try catch ,一旦外層loop(系統奔潰)檢測到這個事件,然后執行我們自己的loop回圈
try {
Looper.loop();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(CatchExceptionApp.this,
"main-Thread 拋出了例外",
Toast.LENGTH_SHORT).show();
}
}
}
});
}
}
執行一下效果看看:

至此,最后我們就解決了拋出例外導致 App crash 的問題了,
難道問題就這樣終結了嗎?當然沒有那么快就結束,這里給主執行緒的Looper 發送 loop 回圈都是主執行緒操作的,那么子執行緒如果拋出例外怎么辦呢,這么處理應該也是會 crash 吧,那再做個實驗吧:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnCrash: // 子執行緒拋出例外不奔潰處理
childThreadCrash();
break;
}
}
/**
* 人為造一個crash
*/
private void childThreadCrash() {
new Thread(new Runnable() {
@Override
public void run() {
// tvMsg.setText("人為造一個crash");
int i = 1 / 0;
Log.d("MainActivity", "i:" + i);
}
}).start();
}
執行一下效果看看:

不出意料,確實是直接crash的,那這個時候該怎么辦呢?
剛才說的Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());似乎也不行,這是設定當前 Thread 的方法,總不能給每個 Thread 都設定一個吧,這肯定不可取,不過這時 Thead 的全域靜態的 UncaughtExceptionHandler 物件就派上用場了
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
ThreadGroup 里面最侄訓呼叫到他的方法,一開始在 RunTimeInit 里面初始化的,既然這樣,那我們直接覆寫這個物件就可以
/**
* Created by lxb on 2020/12/11
* 全域捕獲例外
*/
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public static CatchExceptionApp mApp;
@Override
public void onCreate() {
super.onCreate();
mApp = this;
Handler handler = new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 未捕獲的例外導致APP Crash 后,通過while 讓自己創建的loop 再次作業
while (true) {
//給自己創建的loop 添加 try catch ,一旦外層loop(系統奔潰)檢測到這個事件,然后執行我們自己的loop回圈
try {
Looper.loop();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(CatchExceptionApp.this,
"main-Thread 拋出了例外",
Toast.LENGTH_SHORT).show();
}
}
}
});
// 子執行緒Crash,需要添加這個
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler());
}
}
這樣就解決了子執行緒拋出例外而crash的問題了,
總結
今天主要就說了一件事:如何捕獲程式中的例外不讓APP崩潰,從而給用戶帶來最好的體驗,
主要有以下做法:
1、通過在主執行緒里面發送一個訊息,捕獲主執行緒的例外,并在例外發生后繼續呼叫Looper.loop方法,使得主執行緒繼續處理訊息,
2、對于子執行緒的例外,可以通過Thread.setDefaultUncaughtExceptionHandler來攔截,并且子執行緒的停止不會給用戶帶來感知,
3、對于在生命周期內發生的例外,可以通過替換ActivityThread.mH.mCallback的方法來捕獲,并且通過token來結束Activity或者直接殺死行程,
第三點文中未介紹,感興趣的小伙伴可以自行查閱總結,
可能有的朋友會問,為什么要讓程式不崩潰呢?會有哪些情況需要我們進行這樣操作呢?
其實還是有很多時候,有些例外我們無法預料或者給用戶帶來幾乎是無感知的例外,比如:
- 系統的一些bug
- 第三方庫的一些bug
- 不同廠商的手機帶來的一些bug
等等這些情況,我們就可以通過這樣的操作來讓APP犧牲掉這部分的功能來維護系統的穩定性,我們現在的需求采用這種方法就很符合,其他小伙伴可以根據自己App的需求,不過建議最好的方式就是控制代碼質量,盡量減少 crash 的發生,
文章參考
https://www.jianshu.com/p/37f363308d5f
https://developer.aliyun.com/article/63992
https://www.jianshu.com/p/4fa8a9b814b1
https://mp.weixin.qq.com/s/ZzkgnhalwBv9yAwHj8jQVA
知識拓展
ActivityThread和android應用啟動
深入聊聊Android訊息機制中的訊息佇列的設計
Android中為什么主執行緒不會因為Looper.loop()里的死回圈卡死?
震驚!Android子執行緒也能修改UI?
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/240559.html
標籤:其他
