目錄java
前言android
每個Android開發同窗在項目開發過程當中確定都遇到過各式各樣的Crash問題,你們都很是不但願程序發生Crash。web
那麼問題來了,你真的瞭解Crash嗎?面試
最近在思考一個問題,爲何Android程序發生空指針等異常時,會致使應用會崩潰,進程結束。數組
而java web程序發生這些異常,只要有其餘線程還在運行,虛擬機就不會關閉,進程也不會結束。架構
我在App中模擬了一個數組越界異常,Android系統會幫咱們打印異常日誌。app
主線程異常ide
子線程異常函數
每當異常發生的時候,咱們每每都會經過查看日誌來解決。oop
那麼咱們是否是能夠經過查看打印異常日誌的代碼,來找到Android系統是如何拋出這些未捕獲的異常,以及Android在出現未捕獲異常的時候爲何會發生Crash。
咱們找到了com.android.internal.os.RuntimeInit類,這裏咱們僅貼出咱們須要的代碼
public class RuntimeInit { final static String TAG = "AndroidRuntime"; .... 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 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); if (mCrashing) return; mCrashing = true; if (ActivityThread.currentActivityThread() != null) { ActivityThread.currentActivityThread().stopProfiling(); } ActivityManager.getService().handleApplicationCrash( mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e)); } catch (Throwable t2) { if (t2 instanceof DeadObjectException) { } else { try { Clog_e(TAG, "Error reporting crash", t2); } catch (Throwable t3) { } } } finally { //殺死進程 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) { } } } .... } protected static final void commonInit() { //設置異常處理回調 LoggingHandler loggingHandler = new LoggingHandler(); Thread.setUncaughtExceptionPreHandler(loggingHandler); Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler)); .... }
RuntimeInit有兩個的內部類,LoggingHandler和KillApplicationHandler。
很顯然,LoggingHandler的做用是打印異常日誌,而KillApplicationHandler就是App發生Crash的真正緣由,其內部調用了Process.killProcess(Process.myPid())來殺死發生Uncaught異常的進程。
咱們還發現,這兩個內部類都實現了Thread.UncaughtExceptionHandler接口。
分別經過Thread.setUncaughtExceptionPreHandler和Thread.setDefaultUncaughtExceptionHandler方法進行註冊。
小結:Uncaught異常發生時會終止線程,此時,系統便會通知UncaughtExceptionHandler,告訴它被終止的線程以及對應的異常, 而後便會調用uncaughtException函數。
若是該handler沒有被顯式設置,則會調用對應線程組的默認handler。若是咱們要捕獲該異常,必須實現咱們本身的handler。
上面說到了咱們能夠在應用層調用Thread.setDefaultUncaughtExceptionHandler來實現全部線程的Uncaught異常的監聽,而且會覆蓋系統的默認實現的KillApplicationHandler,這樣咱們就能夠作到讓線程發生Uncaught異常的時候只是當前殺死線程,而不會殺死整個進程。
這適用於咱們的子線程發生Uncaught異常,若是咱們的主線程發生Uncaught異常呢?
主線程都被銷燬了,這和Crash彷佛就沒什麼區別的。
那麼咱們有辦法讓主線程發生Uncaught異常也不會發生Crash嗎?
答案是有的,但在講如何實現以前咱們先來介紹一些知識點。
咱們知道Java程序開始於一個Main函數,若是隻是順序執行有限任務很快這個Main函數所在的線程就結束了。
如何來保持Main函數一直存活並不斷的處理已知或未知的任務呢?
若是熟悉Android Handler機制的話,咱們會了解到整個Android系統實際上是消息驅動的。
Looper內部是一個死循環,不斷地MessageQueue內部取出消息,由消息來通知作什麼任務。
好比收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終會經過反射機制,建立Activity實例,而後再執行Activity.onCreate()等方法;
再好比收到msg=H.PAUSE_ACTIVITY,則調用ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。
public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; ... for (;;) { //從消息隊列中取出Message Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } //派發發消息到對應的Handler,target就是Handler的實例 msg.target.dispatchMessage(msg); .... //釋放消息佔據的資源 msg.recycleUnchecked(); } }
那麼咱們有沒有想過一個問題,Looper.loop是在ActiivtyThread被調用的,也就是主線程中,那麼主線程中死循環爲何不會致使應用卡死呢?
這裏就涉及到Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在Looper.loop()的queue.next()中的nativePollOnce()方法,此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,經過往pipe管道寫端寫入數據來喚醒主線程工做。這裏採用的epoll機制,是一種IO多路複用機制,能夠同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則馬上通知相應程序進行讀或寫操做,本質同步I/O,即讀寫是阻塞的。因此說,主線程大多數時候都是處於休眠狀態,並不會消耗大量CPU資源。當收到不一樣Message時則採用相應措施:一旦退出消息循環,那麼你的程序也就能夠退出了。從消息隊列中取消息可能會阻塞,取到消息會作出相應的處理。若是某個消息處理時間過長,就可能會影響UI線程的刷新速率,形成卡頓的現象
在子線程中,若是手動爲其建立了Looper,那麼在全部的事情完成之後應該調用quit()方法來終止消息循環,不然這個子線程就會一直處於等待(阻塞)狀態,而若是退出Looper之後,這個線程就會馬上(執行全部方法並)終止,所以建議不須要的時候終止Looper
簡單總結一下就是當沒有消息時,native層的方法作了阻塞處理,因此Looper.loop()死循環不會卡死應用。
咱們整個系統都是基於消息機制,再回過頭去看一眼上面的主線程異常日誌堆棧信息,是否是會通過Looper.loop(),因此其實咱們只須要try catch Looper.loop()便可捕獲主線程異常。
代碼以下所示
public class CrashCatch { private CrashHandler mCrashHandler; private static CrashCatch mInstance; private CrashCatch(){ } private static CrashCatch getInstance(){ if(mInstance == null){ synchronized (CrashCatch.class){ if(mInstance == null){ mInstance = new CrashCatch(); } } } return mInstance; } public static void init(CrashHandler crashHandler){ getInstance().setCrashHandler(crashHandler); } private void setCrashHandler(CrashHandler crashHandler){ mCrashHandler = crashHandler; //主線程異常攔截 new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { for (;;) { try { Looper.loop(); } catch (Throwable e) { if (mCrashHandler != null) { //處理異常 mCrashHandler.handlerException(Looper.getMainLooper().getThread(), e); } } } } }); //全部線程異常攔截,因爲主線程的異常都被咱們catch住了,因此下面的代碼攔截到的都是子線程的異常 Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { if(mCrashHandler!=null){ //處理異常 mCrashHandler.handlerException(t,e); } } }); } public interface CrashHandler{ void handlerException(Thread t,Throwable e); } }
原理很簡單,就是經過Handler往主線程的MessageQueue中添加一個Runnable,當主線程執行到該Runnable時,會進入咱們的while死循環。
若是while內部是空的就會致使代碼卡在這裏,最終致使ANR,但咱們在while死循環中又調用了Looper.loop(),這就致使主線程又開始不斷的讀取queue中的Message並執行,這樣就能夠保證之後主線程的全部異常都會從咱們手動調用的Looper.loop()處拋出,一旦拋出就會被try{}catch捕獲,這樣主線程就不會crash了。
若是沒有這個while的話那麼主線程下次拋出異常時咱們就又捕獲不到了,這樣App就又crash了,因此咱們要經過while讓每次crash發生後都再次進入消息循環,while的做用僅限於每次主線程拋出異常後迫使主線程再次進入消息循環。
爲何要經過new Handler.post方式而不是直接在主線程中任意位置執行 while (true) { try { Looper.loop(); } catch (Throwable e) {} }。
這是由於該方法是個死循環,若在主線程中,好比在Activity的onCreate中執行時會致使while後面的代碼得不到執行,Activity的生命週期也就不能完整執行,經過Handler.post方式能夠保證不影響該條消息中後面的邏輯。
使用起來也很是簡單
CrashCatch.getInstance().setCrashHandler(new CrashHandler(){ @Override void handlerException(Thread t,Throwable e){ //try catch 以防handlerException內部再次拋出異常,致使循環調用handlerException try{ //TODO 實現本身的異常處理邏輯 }catch(Exeception e){ } } })
不少時候因爲一些微不足道的bug致使app崩潰很惋惜,android默認的異常殺進程機制簡單粗暴,但不少時候讓app崩潰其實並非一個特別好的選擇。
有些bug多是系統bug,對於這些難以預料的系統bug咱們很差繞過,還有一些bug是咱們本身編碼形成的,對於有些bug來講直接忽略掉的話可能只是致使部分不重要的功能無法使用而已,又或者對用戶來講徹底沒有影響,這種狀況總比每次都崩潰要好不少。
咱們還能夠捕獲到異常後作一些本身的邏輯判斷。
本文主要講原理,具體你們如何使用如何取捨,仍是視本身項目的實際狀況而定。
在這我也分享一份本身收錄整理的 Android學習PDF+架構視頻+面試文檔+源碼筆記 ,還有高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料這些都是我閒暇還會反覆翻閱的精品資料。在腦圖中,每一個知識點專題都配有相對應的實戰項目,能夠有效的幫助你們掌握知識點。
總之也是在這裏幫助你們學習提高進階,也節省你們在網上搜索資料的時間來學習,也能夠分享給身邊好友一塊兒學習
若是你有須要的話,能夠點贊+評論,關注我, 加Vx:15388039515(備註思否,須要資料)