前言
關於攔截異常,想必你們都知道能夠經過Thread.setDefaultUncaughtExceptionHandler
來攔截App中發生的異常,而後再進行處理。java
因而,我有了一個不成熟的想法。。。android
讓個人APP永不崩潰
既然咱們能夠攔截崩潰,那咱們直接把APP中全部的異常攔截了,不殺死程序。這樣一個不會崩潰的APP用戶體驗不是槓槓的?git
- 有人聽了搖搖頭表示不贊同,這不小光跑來問我了:
「老鐵,出現崩潰是要你解決它不是掩蓋它!!」github
- 我拿把扇子扇了幾下,有點冷可是故做鎮定的說:
「這位老哥,你能夠把異常上傳到本身的服務器處理啊,你能拿到你的崩潰緣由,用戶也不會由於異常致使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("子線程異常") } } } }
小光一頓操做,寫下了整套代碼,爲了驗證它的猜測,寫了兩種觸發異常的狀況:子線程崩潰和主線程崩潰。ide
- 運行,點擊按鈕2,觸發子線程異常崩潰:
「咦,還真沒啥影響,程序能繼續正常運行」oop
- 而後點擊按鈕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或者直接殺死進程。
可能有的朋友會問,爲何要讓程序不崩潰呢?會有哪些狀況須要咱們進行這樣操做呢?
其實仍是有不少時候,有些異常咱們沒法預料
或者給用戶帶來幾乎是無感知
的異常,好比:
- 系統的一些bug
- 第三方庫的一些bug
- 不一樣廠商的手機帶來的一些bug
等等這些狀況,咱們就能夠經過這樣的操做來讓APP
犧牲掉這部分的功能來維護系統的穩定性。
參考
Cockroach 一文讀懂 Handler 機制全家桶 zyogte進程(Java篇) wanAndroid
拜拜
好了,到了說再見的時候了。
最後給你們推薦一個劇—棋魂,嘿嘿,小光就是裏面的主角。
這些優秀的開源庫又未嘗不是指引咱們前行進步的光呢~
感謝你們的閱讀,有一塊兒學習的小夥伴能夠關注下個人公衆號——碼上積木❤️❤️
每日三問知識點/面試題,聚沙成塔。