1. 程式人生 > >能否讓APP永不崩潰—小光與我的對決

能否讓APP永不崩潰—小光與我的對決

## 前言 關於**攔截異常**,想必大家都知道可以通過`Thread.setDefaultUncaughtExceptionHandler`來攔截App中發生的異常,然後再進行處理。 於是,我有了一個不成熟的想法。。。 ## 讓我的APP永不崩潰 既然我們可以攔截崩潰,那我們直接把APP中所有的異常攔截了,不殺死程式。這樣一個**不會崩潰的APP**使用者體驗不是槓槓的? * 有人聽了搖搖頭表示不贊同,這不小光跑來問我了: “老鐵,出現崩潰是要你解決它不是掩蓋它!!” * 我拿把扇子扇了幾下,有點冷但是**故作鎮定**的說: “這位老哥,你可以把異常上傳到自己的伺服器處理啊,你能拿到你的崩潰原因,使用者也不會因為異常導致APP崩潰,這不挺好?” * 小光有點生氣的說: “這樣肯定有問題,聽著就**不靠譜**,哼,我去試試看” ## 小光的實驗 於是小光按照網上一個`小博主—積木`的文章,寫出了以下捕獲異常的程式碼: ```kotlin //定義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了” ![主執行緒崩潰](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/900ad834cdd740d8802da87783659439~tplv-k3u1fbpfcp-watermark.image) “果然有問題,但是為啥主執行緒會出問題呢?我得先搞懂再去找老鐵對峙。” ## 小光的思考(異常原始碼分析) 首先科普下java中的異常,包括`執行時異常`和`非執行時異常`: * 執行時異常。是`RuntimeException`類及其子類的異常,是非受檢異常,比如系統異常或者是程式邏輯異常,我們常遇到的有`NullPointerException、IndexOutOfBoundsException`等。遇到這種異常,`Java Runtime`會停止執行緒,列印異常,並且會停止程式執行,也就是我們常說的程式崩潰。 * 非執行時異常。是屬於`Exception`類及其子類,是受檢異常,`RuntimeException`以外的異常。這類異常在程式中必須進行處理,如果不處理程式都無法正常編譯,比如`NoSuchFieldException,IllegalAccessException`這種。 ok,也就是說我們丟擲一個`RuntimeException`異常之後,所在的執行緒會被停止。如果主執行緒中丟擲這個異常,那麼主執行緒就會被停止,所以APP就會卡住無法正常操作,時間久了就會`ANR`。而子執行緒崩潰了並不會影響主執行緒也就是UI執行緒的操作,所以使用者還能正常使用。 這樣好像就說的通了。 等等,那為什麼遇到`setDefaultUncaughtExceptionHandler`就不會崩潰了呢? 我們還得從異常的原始碼開始說起: 一般情況下,一個應用中所使用的執行緒都是在同一個執行緒組,而在這個執行緒組裡只要有一個執行緒出現未被捕獲異常的時候,JAVA 虛擬機器就會呼叫當前執行緒所線上程組中的 `uncaughtException()`方法。 ```java // 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`方法,這個方法裡會進行一些應用的初始化工作: ```java 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); } ``` 而關於異常處理器,就在這個通用的配置初始化方法當中: ```java 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`。 ```java 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`就是我們平時遇到崩潰問題,所看到的崩潰日誌?進去瞅瞅: ```java 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打印出來了。來張崩潰日誌圖給大家對對看: ![崩潰日誌圖](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3ad0459404f4464b734be2b6a076381~tplv-k3u1fbpfcp-watermark.image) 好了,回到正軌,所以我們通過`setDefaultUncaughtExceptionHandler`方法設定了我們自己的崩潰處理器,就把之前應用設定的這個崩潰處理器給頂掉了,然後我們又沒有做任何處理,自然程式就不會崩潰了,來張總結圖。 ![崩潰呼叫圖](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9fa6f2867b044ed984daaec7af9b9ca4~tplv-k3u1fbpfcp-watermark.image) ## 小光又來找我對峙了 * 搞清楚這一切的小光又來找我了: “老鐵,你瞅瞅,這是我寫的`Demo`和總結的資料,你那套根本行不通,主執行緒崩潰就GG了,我就說有問題吧” * 我繼續**故作鎮定**: “老哥,我上次忘記說了,只加這個`UncaughtExceptionHandler`可不行,還得加一段程式碼,發給你,回去試試吧” ```kotlin Handler(Looper.getMainLooper()).post { while (true) { try { Looper.loop() } catch (e: Throwable) { } } } ``` “這,,能行嗎” ## 小光再次的實驗 小光把上述程式碼加到了程式裡面(Application—onCreate),再次執行: 我去,`真的沒問題了`,點選主執行緒崩潰後,還是可以正常操作app,這又是什麼原理呢? ## 小光的再次思考(攔截主執行緒崩潰的方案思想) 我們都知道,在主執行緒中維護著`Handler`的一套機制,在應用啟動時就做好了`Looper`的建立和初始化,並且呼叫了`loop`方法開始了訊息的迴圈處理。應用在使用過程中,主執行緒的所有操作比如事件點選,列表滑動等等都是在這個迴圈中完成處理的,其本質就是將訊息加入`MessageQueue`佇列,然後迴圈從這個佇列中取出訊息並處理,如果沒有訊息處理的時候,就會依靠epoll機制掛起等待喚醒。貼一下我濃縮的`loop`程式碼: ```java public static void loop() { final Looper me = myLooper(); final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); msg.target.dispatchMessage(msg); } } ``` 一個死迴圈,不斷取訊息處理訊息。再回頭看看剛才加的程式碼: ```kotlin Handler(Looper.getMainLooper()).post { while (true) { //主執行緒異常攔截 try { Looper.loop() } catch (e: Throwable) { } } } ``` 我們通過`Handler`往主執行緒傳送了一個`runnable`任務,然後在這個`runnable`中加了一個死迴圈,死迴圈中執行了`Looper.loop()`進行訊息迴圈讀取。這樣就會導致後續所有的主執行緒訊息都會走到我們這個`loop`方法中進行處理,也就是一旦發生了主執行緒崩潰,那麼這裡就可以進行異常捕獲。同時因為我們寫的是while死迴圈,那麼捕獲異常後,又會開始新的`Looper.loop()`方法執行。這樣主執行緒的Looper就可以一直正常讀取訊息,主執行緒就可以一直正常運行了。 文字說不清楚的圖片來幫我們: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1848f19df9c4395a94cb79a729929cc~tplv-k3u1fbpfcp-watermark.image) 同時之前`CrashHandler`的邏輯可以保證子執行緒也是不受崩潰影響,所以兩段程式碼都加上,齊活了。 但是小光還不服氣,他又想到了一種崩潰情況。。。 ## 小光又又又一次實驗 ```kotlin 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`或者殺死程序操作了。” 主要程式碼: ```kotlin 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。 * 殺死程序,這個應該大家都熟悉 ```java Process.killProcess(Process.myPid()) exitProcess(10) ``` * finish掉Activity 這裡又要分析下Activity的`finish`流程了,簡單說下,以`android29`的原始碼為例。 ```java 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`可以用,那我麼你就從這方面入手。回到剛才我們處理訊息的原始碼中,看看能不能找到什麼線索: ```java 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的銷燬就行啦: ```java 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](https://github.com/android-notes/Cockroach) [一文讀懂 Handler 機制全家桶](https://juejin.cn/post/6901682664617705485) [zyogte程序(Java篇)](https://www.jianshu.com/p/abaff702f357) [wanAndroid](https://www.wanandroid.com/wenda/show/13485) ## 拜拜 好了,到了說再見的時候了。 最後給大家推薦一個劇—**棋魂**,嘿嘿,小光就是裡面的主角。 這些優秀的開源庫又何嘗不是指引我們前行進步的光呢~ > 有一起學習的小夥伴可以關注下❤️我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。公眾號回覆111可獲得面試題《思考與解答》以往期刊。 ![](https://s1.ax1x.com/2020/11/05/BRV7Mq.