前言
關于攔截例外,想必大家都知道可以通過Thread.setDefaultUncaughtExceptionHandler來攔截App中發生的例外,然后再進行處理,
于是,我有了一個不成熟的想法,,,
讓我的APP永不崩潰
既然我們可以攔截崩潰,那我們直接把APP中所有的例外攔截了,不殺死程式,這樣一個不會崩潰的APP用戶體驗不是杠杠的?
- 有人聽了搖搖頭表示不贊同,這不小光跑來問我了:
“老鐵,出現崩潰是要你解決它不是掩蓋它!!”
- 我拿把扇子扇了幾下,有點冷但是故作鎮定的說:
“這位老哥,你可以把例外上傳到自己的服務器處理啊,你能拿到你的崩潰原因,用戶也不會因為例外導致APP崩潰,這不挺好?”
- 小光有點生氣的說:
“這樣肯定有問題,聽著就不靠譜,哼,我去試試看”
小光的實驗
于是小光按照網上一個小博主—積木的文章,寫出了以下捕獲例外的代碼:
//定義CrashHandler
class CrashHandler private constructor(): Thread.UncaughtExceptionHandler {
private var context: Context? = null
fun init(context: Context?) {
this.context = context
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(t: Thread, e: Throwable) {}
companion object {
val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
CrashHandler() }
}
}
//Application中初始化
class MyApplication : Application(){
override fun onCreate() {
super.onCreate()
CrashHandler.instance.init(this)
}
}
//Activity中觸發例外
class ExceptionActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exception)
btn.setOnClickListener {
throw RuntimeException("主執行緒例外")
}
btn2.setOnClickListener {
thread {
throw RuntimeException("子執行緒例外")
}
}
}
}
小光一頓操作,寫下了整套代碼,為了驗證它的猜想,寫了兩種觸發例外的情況:子執行緒崩潰和主執行緒崩潰,
- 運行,點擊按鈕2,觸發子執行緒例外崩潰:
“咦,還真沒啥影響,程式能繼續正常運行”
- 然后點擊按鈕1,觸發主執行緒例外崩潰:
“嘿嘿,卡住了,再點幾下,直接ANR了”
“果然有問題,但是為啥主執行緒會出問題呢?我得先搞懂再去找老鐵對峙,”
小光的思考(例外原始碼分析)
首先科普下java中的例外,包括運行時例外和非運行時例外:
-
運行時例外,是
RuntimeException類及其子類的例外,是非受檢例外,比如系統例外或者是程式邏輯例外,我們常遇到的有NullPointerException、IndexOutOfBoundsException等,遇到這種例外,Java Runtime會停止執行緒,列印例外,并且會停止程式運行,也就是我們常說的程式崩潰, -
非運行時例外,是屬于
Exception類及其子類,是受檢例外,RuntimeException以外的例外,這類例外在程式中必須進行處理,如果不處理程式都無法正常編譯,比如NoSuchFieldException,IllegalAccessException這種,
ok,也就是說我們拋出一個RuntimeException例外之后,所在的執行緒會被停止,如果主執行緒中拋出這個例外,那么主執行緒就會被停止,所以APP就會卡住無法正常操作,時間久了就會ANR,而子執行緒崩潰了并不會影響主執行緒也就是UI執行緒的操作,所以用戶還能正常使用,
這樣好像就說的通了,
等等,那為什么遇到setDefaultUncaughtExceptionHandler就不會崩潰了呢?
我們還得從例外的原始碼開始說起:
一般情況下,一個應用中所使用的執行緒都是在同一個執行緒組,而在這個執行緒組里只要有一個執行緒出現未被捕獲例外的時候,JAVA 虛擬機就會呼叫當前執行緒所在執行緒組中的 uncaughtException()方法,
// ThreadGroup.java
private final ThreadGroup parent;
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);
}
}
}
parent表示當前執行緒組的父級執行緒組,所以最后還是會呼叫到這個方法中,接著看后面的代碼,通過getDefaultUncaughtExceptionHandler獲取到了系統默認的例外處理器,然后呼叫了uncaughtException方法,那么我們就去找找本來系統中的這個例外處理器——UncaughtExceptionHandler,
這就要從APP的啟動流程說起了,之前也說過,所有的Android行程都是由zygote行程fork而來的,在一個新行程被啟動的時候就會呼叫zygoteInit方法,這個方法里會進行一些應用的初始化作業:
public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
if (RuntimeInit.DEBUG) {
Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
//日志重定向
RuntimeInit.redirectLogStreams();
//通用的配置初始化
RuntimeInit.commonInit();
// zygote初始化
ZygoteInit.nativeZygoteInit();
//應用相關初始化
return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
}
而關于例外處理器,就在這個通用的配置初始化方法當中:
protected static final void commonInit() {
if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
//設定例外處理器
LoggingHandler loggingHandler = new LoggingHandler();
Thread.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
//設定時區
TimezoneGetter.setInstance(new TimezoneGetter() {
@Override
public String getId() {
return SystemProperties.get("persist.sys.timezone");
}
});
TimeZone.setDefault(null);
//log配置
LogManager.getLogManager().reset();
//***
initialized = true;
}
找到了吧,這里就設定了應用默認的例外處理器——KillApplicationHandler,
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
private final LoggingHandler mLoggingHandler;
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
ensureLogging(t, e);
//...
// 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);
}
}
private void ensureLogging(Thread t, Throwable e) {
if (!mLoggingHandler.mTriggered) {
try {
mLoggingHandler.uncaughtException(t, e);
} catch (Throwable loggingThrowable) {
// Ignored.
}
}
}
看到這里,小光欣慰一笑,被我逮到了吧,在uncaughtException回呼方法中,會執行一個handleApplicationCrash方法進行例外處理,并且最后都會走到finally中進行行程銷毀,Try everything to make sure this process goes away,所以程式就崩潰了,
關于我們平時在手機上看到的崩潰提示彈窗,就是在這個handleApplicationCrash方法中彈出來的,不僅僅是java崩潰,還有我們平時遇到的native_crash、ANR等例外都會最后走到handleApplicationCrash方法中進行崩潰處理,
另外有的朋友可能發現了構造方法中,傳入了一個LoggingHandler,并且在uncaughtException回呼方法中還呼叫了這個LoggingHandler的uncaughtException方法,難道這個LoggingHandler就是我們平時遇到崩潰問題,所看到的崩潰日志?進去瞅瞅:
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
public volatile boolean mTriggered = false;
@Override
public void uncaughtException(Thread t, Throwable e) {
mTriggered = true;
if (mCrashing) return;
if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
} else {
StringBuilder message = new StringBuilder();
message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
final String processName = ActivityThread.currentProcessName();
if (processName != null) {
message.append("Process: ").append(processName).append(", ");
}
message.append("PID: ").append(Process.myPid());
Clog_e(TAG, message.toString(), e);
}
}
}
private static int Clog_e(String tag, String msg, Throwable tr) {
return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
}
這可不就是嗎?將崩潰的一些資訊——比如執行緒,行程,行程id,崩潰原因等等通過Log列印出來了,來張崩潰日志圖給大家對對看:
好了,回到正軌,所以我們通過setDefaultUncaughtExceptionHandler方法設定了我們自己的崩潰處理器,就把之前應用設定的這個崩潰處理器給頂掉了,然后我們又沒有做任何處理,自然程式就不會崩潰了,來張總結圖,
小光又來找我對峙了
- 搞清楚這一切的小光又來找我了:
“老鐵,你瞅瞅,這是我寫的Demo和總結的資料,你那套根本行不通,主執行緒崩潰就GG了,我就說有問題吧”
- 我繼續故作鎮定:
“老哥,我上次忘記說了,只加這個UncaughtExceptionHandler可不行,還得加一段代碼,發給你,回去試試吧”
Handler(Looper.getMainLooper()).post {
while (true) {
try {
Looper.loop()
} catch (e: Throwable) {
}
}
}
“這,,能行嗎”
小光再次的實驗
小光把上述代碼加到了程式里面(Application—onCreate),再次運行:
我去,真的沒問題了,點擊主執行緒崩潰后,還是可以正常操作app,這又是什么原理呢?
小光的再次思考(攔截主執行緒崩潰的方案思想)
我們都知道,在主執行緒中維護著Handler的一套機制,在應用啟動時就做好了Looper的創建和初始化,并且呼叫了loop方法開始了訊息的回圈處理,應用在使用程序中,主執行緒的所有操作比如事件點擊,串列滑動等等都是在這個回圈中完成處理的,其本質就是將訊息加入MessageQueue佇列,然后回圈從這個佇列中取出訊息并處理,如果沒有訊息處理的時候,就會依靠epoll機制掛起等待喚醒,貼一下我濃縮的loop代碼:
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next();
msg.target.dispatchMessage(msg);
}
}
一個死回圈,不斷取訊息處理訊息,再回頭看看剛才加的代碼:
Handler(Looper.getMainLooper()).post {
while (true) {
//主執行緒例外攔截
try {
Looper.loop()
} catch (e: Throwable) {
}
}
}
我們通過Handler往主執行緒發送了一個runnable任務,然后在這個runnable中加了一個死回圈,死回圈中執行了Looper.loop()進行訊息回圈讀取,這樣就會導致后續所有的主執行緒訊息都會走到我們這個loop方法中進行處理,也就是一旦發生了主執行緒崩潰,那么這里就可以進行例外捕獲,同時因為我們寫的是while死回圈,那么捕獲例外后,又會開始新的Looper.loop()方法執行,這樣主執行緒的Looper就可以一直正常讀取訊息,主執行緒就可以一直正常運行了,
文字說不清楚的圖片來幫我們:
同時之前CrashHandler的邏輯可以保證子執行緒也是不受崩潰影響,所以兩段代碼都加上,齊活了,
但是小光還不服氣,他又想到了一種崩潰情況,,,
小光又又又一次實驗
class Test2Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exception)
throw RuntimeException("主執行緒例外")
}
}
誒,我直接在onCreate里面給你拋出個例外,運行看看:
黑漆漆的一片~沒錯,黑屏了,
最后的對話(Cockroach庫思想)
- 看到這一幕,我主動找到了小光:
“這種情況確實比較麻煩了,如果直接在Activity生命周期內拋出例外,會導致界面繪制無法完成,Activity無法被正確啟動,就會白屏或者黑屏了
這種嚴重影響到用戶體驗的情況還是建議直接殺死APP,因為很有可能會對其他的功能模塊造成影響,或者如果某些Activity不是很重要,也可以只finish這個Activity,”
- 小光思索地問:
“那么怎么分辨出這種生命周期內發生崩潰的情況呢?”
“這就要通過反射了,借用Cockroach開源庫中的思想,由于Activity的生命周期都是通過主執行緒的Handler進行訊息處理,所以我們可以通過反射替換掉主執行緒的Handler中的Callback回呼,也就是ActivityThread.mH.mCallback,然后針對每個生命周期對應的訊息進行trycatch捕獲例外,然后就可以進行finishActivity或者殺死行程操作了,”
主要代碼:
Field mhField = activityThreadClass.getDeclaredField("mH");
mhField.setAccessible(true);
final Handler mhHandler = (Handler) mhField.get(activityThread);
Field callbackField = Handler.class.getDeclaredField("mCallback");
callbackField.setAccessible(true);
callbackField.set(mhHandler, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (Build.VERSION.SDK_INT >= 28) {
//android 28之后的生命周期處理
final int EXECUTE_TRANSACTION = 159;
if (msg.what == EXECUTE_TRANSACTION) {
try {
mhHandler.handleMessage(msg);
} catch (Throwable throwable) {
//殺死行程或者殺死Activity
}
return true;
}
return false;
}
//android 28之前的生命周期處理
switch (msg.what) {
case RESUME_ACTIVITY:
//onRestart onStart onResume回呼這里
try {
mhHandler.handleMessage(msg);
} catch (Throwable throwable) {
sActivityKiller.finishResumeActivity(msg);
notifyException(throwable);
}
return true;
代碼貼了一部分,但是原理大家應該都懂了吧,就是通過替換主執行緒Handler的Callback,進行宣告周期的例外捕獲,
接下來就是進行捕獲后的處理作業了,要不殺死行程,要么殺死Activity,
- 殺死行程,這個應該大家都熟悉
Process.killProcess(Process.myPid())
exitProcess(10)
- finish掉Activity
這里又要分析下Activity的finish流程了,簡單說下,以android29的原始碼為例,
private void finish(int finishTask) {
if (mParent == null) {
if (false) Log.v(TAG, "Finishing self: token=" + mToken);
try {
if (resultData != null) {
resultData.prepareToLeaveProcess(this);
}
if (ActivityTaskManager.getService()
.finishActivity(mToken, resultCode, resultData, finishTask)) {
mFinished = true;
}
}
}
}
@Override
public final boolean finishActivity(IBinder token, int resultCode, Intent resultData,
int finishTask) {
return mActivityTaskManager.finishActivity(token, resultCode, resultData, finishTask);
}
從Activity的finish原始碼可以得知,最終是呼叫到ActivityTaskManagerService的finishActivity方法,這個方法有四個引數,其中有個用來標識Activity的引數也就是最重要的引數——token,所以去原始碼里面找找token~
由于我們捕獲的地方是在handleMessage回呼方法中,所以只有一個引數Message可以用,那我么你就從這方面入手,回到剛才我們處理訊息的原始碼中,看看能不能找到什么線索:
class H extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
break;
}
}
}
public void execute(ClientTransaction transaction) {
final IBinder token = transaction.getActivityToken();
executeCallbacks(transaction);
executeLifecycleState(transaction);
mPendingActions.clear();
log("End resolving transaction");
}
可以看到在原始碼中,Handler是怎么處理EXECUTE_TRANSACTION訊息的,獲取到msg.obj物件,也就是ClientTransaction類實體,然后呼叫了execute方法,而在execute方法中,,,咦咦咦,這不就是token嗎?
(找到的過于快速了哈,主要是activity啟動銷毀這部分的原始碼解說并不是今天的重點,所以就一筆帶過了)
找到token,那我們就通過反射進行Activity的銷毀就行啦:
private void finishMyCatchActivity(Message message) throws Throwable {
ClientTransaction clientTransaction = (ClientTransaction) message.obj;
IBinder binder = clientTransaction.getActivityToken();
Method getServiceMethod = ActivityManager.class.getDeclaredMethod("getService");
Object activityManager = getServiceMethod.invoke(null);
Method finishActivityMethod = activityManager.getClass().getDeclaredMethod("finishActivity", IBinder.class, int.class, Intent.class, int.class);
finishActivityMethod.setAccessible(true);
finishActivityMethod.invoke(activityManager, binder, Activity.RESULT_CANCELED, null, 0);
}
啊,終于搞定了,但是小光還是一臉疑惑的看著我:
“我還是去看Cockroach庫的原始碼吧~”
“我去,,”
總結
今天主要就說了一件事:如何捕獲程式中的例外不讓APP崩潰,從而給用戶帶來最好的體驗,主要有以下做法:
- 通過在主執行緒里面發送一個訊息,捕獲主執行緒的例外,并在例外發生后繼續呼叫
Looper.loop方法,使得主執行緒繼續處理訊息, - 對于子執行緒的例外,可以通過
Thread.setDefaultUncaughtExceptionHandler來攔截,并且子執行緒的停止不會給用戶帶來感知, - 對于在生命周期內發生的例外,可以通過替換
ActivityThread.mH.mCallback的方法來捕獲,并且通過token來結束Activity或者直接殺死行程,但是這種辦法要適配不同SDK版本的原始碼才行,所以慎用,需要的可以看文末Cockroach庫原始碼,
可能有的朋友會問,為什么要讓程式不崩潰呢?會有哪些情況需要我們進行這樣操作呢?
其實還是有很多時候,有些例外我們無法預料或者給用戶帶來幾乎是無感知的例外,比如:
- 系統的一些bug
- 第三方庫的一些bug
- 不同廠商的手機帶來的一些bug
等等這些情況,我們就可以通過這樣的操作來讓APP犧牲掉這部分的功能來維護系統的穩定性,
參考
Cockroach
一文讀懂 Handler 機制全家桶
zyogte行程(Java篇)
wanAndroid
拜拜
好了,到了說再見的時候了,
最后給大家推薦一個劇—棋魂,嘿嘿,小光就是里面的主角,
這些優秀的開源庫又何嘗不是指引我們前行進步的光呢~
有一起學習的小伙伴可以關注下??我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識,公眾號回復111可獲得面試題《思考與解答》以往期刊,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/233011.html
標籤:Android

