開發與維運

04.Android崩潰Crash庫之Loop攔截崩潰和ANR

目錄總結

  • 01.能否利用Looper攔截崩潰
  • 02.思考幾個問題分析
  • 03.App啟動時自動開啟Looper
  • 04.攔截主進程崩潰

前沿

01.能否利用Looper攔截崩潰

  • 問題思考一下

    • 能否基於 Handler 和 Looper 攔截全局崩潰(主線程),避免 APP 退出。
    • 能否基於 Handler 和 Looper 實現 ANR 監控。
  • 測試代碼如下所示

    public class App extends Application {
        @Override
        public void onCreate() {
            super.onCreate();
            CrashTestDemo.test();
        }
    }
    
    //測試代碼
    public class CrashTestDemo {
    
        private static long startWorkTimeMillis = 0L;
        public static void test(){
            Looper.getMainLooper().setMessageLogging(new Printer() {
                @Override
                public void println(String it) {
                    if (it.startsWith(">>>>> Dispatching to Handler")) {
                        startWorkTimeMillis = System.currentTimeMillis();
                    } else if (it.startsWith("<<<<< Finished to Handler")) {
                        long duration = System.currentTimeMillis() - startWorkTimeMillis;
                        if (duration > 100) {
                            Log.e("Application---主線程執行耗時過長","$duration 毫秒,$it");
                        }
                    }
                }
            });
            Handler handler = new Handler(Looper.getMainLooper());
            handler.post(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        try {
                            Looper.loop();
                        } catch (Throwable e){
                            if (e.getMessage()!=null && e.getMessage().startsWith("Unable to start activity")){
                                android.os.Process.killProcess(android.os.Process.myPid());
                                break;
                            }
                            e.printStackTrace();
                            Log.e("Application---Looper---",e.getMessage());
                        }
                    }
                }
            });
            Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    e.printStackTrace();
                    Log.e("Application-----","uncaughtException---異步線程崩潰,自行上報崩潰信息");
                }
            });
        }
    }
  • 通過上面的代碼就可以就可以實現攔截UI線程的崩潰,耗時性能監控。但是也並不能夠攔截所有的異常,如果在Activity的onCreate出現崩潰,導致Activity創建失敗,那麼就會顯示黑屏。

02.思考幾個問題分析

  • 通過上面簡單的代碼,我們就實現崩潰和ANR的攔截和監控,但是我們可能並不知道是為何實現的,包括我們知道出現了ANR,但是我們還需要進一步分析為何處出現ANR,如何解決。
  • 今天分析的問題有:

    • 如何攔截全局崩潰,避免APP退出。如何實現 ANR 監控。攔截到了之後可以做什麼處理,如何優化?

03.App啟動時自動開啟Looper

  • 先從APP啟動開始分析,APP的啟動方法是在ActivityThread中,在main方法中創建了主線程的Looper,也就是當前進程創建。

    • 在main方法的最後調用了 Looper.loop(),在這個方法中處理主線程的任務調度,一旦執行完這個方法就意味著APP被退出了。
    • 如果我們要避免APP被退出,就必須讓APP持續執行Looper.loop()。注意這句話非常重要!!!
    public final class ActivityThread extends ClientTransactionHandler {
        ...
        public static void main(String[] args) {
            ...
            Looper.prepareMainLooper();
            ...
            Looper.loop();
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    }
  • 那進一步分析Looper.loop()方法

    • 在這個方法中寫了一個循環,只有當 queue.next() == null 的時候才退出,看到這裡我們心裡可能會有一個疑問,如果沒有主線程任務,是不是Looper.loop()方法就退出了呢?
    • 實際上queue.next()其實就是一個阻塞的方法,如果沒有任務或沒有主動退出,會一直在阻塞,一直等待主線程任務添加進來。
    • 當隊列有任務,就會打印信息 Dispatching to ...,然後就調用 msg.target.dispatchMessage(msg);執行任務,執行完畢就會打印信息 Finished to ...,我們就可以通過打印的信息來分析 ANR,一旦執行任務超過5秒就會觸發系統提示ANR,但是我們對自己的APP肯定要更加嚴格,我們可以給我們設定一個目標,超過指定的時長就上報統計,幫助我們進行優化。
    public final class Looper {
        final MessageQueue mQueue;
        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 msg = queue.next(); // might block
                if (msg == null) {
                    // No message indicates that the message queue is quitting.
                    return;
                }
                // This must be in a local variable, in case a UI event sets the logger
                final Printer logging = me.mLogging;
                if (logging != null) {
                    logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);
                }
                try {
                    msg.target.dispatchMessage(msg);
                } finally {}
                if (logging != null) {
                    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                }
                msg.recycleUnchecked();
            }
        }
        public void quit() {
            mQueue.quit(false);
        }
    }
  • 如何讓app崩潰後不會退出

    • 如果主線程發生了異常,就會退出循環,意味著APP崩潰,所以我們我們需要進行try-catch,避免APP退出,我們可以在主線程再啟動一個 Looper.loop() 去執行主線程任務,然後try-catch這個Looper.loop()方法,就不會退出。

04.攔截主進程崩潰

  • 攔截主進程崩潰其實也有一定的弊端,因為給用戶的感覺是點擊沒有反應,因為崩潰已經被攔截了。如果是Activity.create崩潰,會出現黑屏問題,所以如果Activity.create崩潰,必須殺死進程,讓APP重啟,避免出現改問題。

    public class MyApplication extends Application {
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            new Handler(getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            Looper.loop();
                        } catch (Throwable e) {
                            e.printStackTrace();
                            // TODO 需要手動上報錯誤到異常管理平臺,比如bugly,及時追蹤問題所在。
                            if (e.getMessage() != null && e.getMessage().startsWith("Unable to start activity")) {
                                // 如果打開Activity崩潰,就殺死進程,讓APP重啟。
                                android.os.Process.killProcess(android.os.Process.myPid());
                                break;
                            }
                        }
                    }
                }
            });
        }
    }

項目地址:https://github.com/yangchong211/YCAndroidTool

Leave a Reply

Your email address will not be published. Required fields are marked *