Android性能優化雜談-如何監控和解決ANR問題?

一、ANR的定義

Android全稱是Application Not Response,即程序無響應。ANR的直觀體驗是用戶在操作APP的過程中,感覺界面卡頓,如果 Android應用的界面線程處於阻塞狀態的時間過長,會觸發“應用無響應”(ANR) 錯誤,如下圖所示,ANR 對話框會爲用戶提供強行退出應用的選項。

image

當點擊了Close app或者由於ANR引起了閃退之後,這時查看Logcat,一般可以發現ANR以及trace.txt等字樣,可以知道出現ANR主要原因是我們在主線程做了太多耗時操作,這時你可以選擇等待按鈕,等待應用程序結束主線程的耗時操作,或者選擇確定按鈕,結束這個應用程序,ANR對於一個應用來說是不能承受之痛,其影響並不比應用發生Crash小。

二、ANR類型

出現ANR的一般有以下幾種類型:

  • KeyDispatchTimeOut :最常見的一種類型,原因是View的按鍵事件或者觸摸事件在特定的時間(5秒)內無法得到響應。
  • BroadcaseTimeOut:原因是BroadcastReceiver的onReceiver()函數運行在主線程中,在特定的時間(10秒)內無法完成處理。
  • ServiceTimeOut:比較少出現的一種類型,原因是Service的各個生命週期函數在特定時間(20秒)內無法完成處理。
  • ContentProviderTimeout:ContentProvider在10S內無法完成處理。

三、ANR原因

從應用的角度上來講,它的原因可以歸結爲以下幾種:

  • 應用在主線程上非常緩慢地執行涉及 I/O 的操作。
  • 應用在主線程上進行長時間的計算。
  • 主線程在對另一個進程進行同步 binder 調用,而後者需要很長時間才能返回。
  • 主線程處於阻塞狀態,等待子線程的長時間耗時操作完成。
  • 主線程在進程中或通過 binder 調用與另一個線程之間發生死鎖。主線程不只是在等待長操作執行完畢,而且處於死鎖狀態。

三、如何檢測和診斷ANR問題

1、開發中檢測ANR

  • StrictMode(嚴格模式)

開發中由於個人原因,多多少少都會可能寫出造成ANR的代碼,要想在開發中及時發現問題,可以使用StrictMode有助於您在開發應用時發現主線程上的意外 I/O 操作。

嚴苛模式是一個避免你不小心把網絡或者IO操作放在UI線程操作的開發工具,從而實現避免ANR。使用嚴苛模式,系統檢測出主線程違例的情況會做出相應的反應,如日誌打印,屏幕閃爍(需要在開發者選項裏面打開啓用嚴格模式)等。

  • 啓用後臺 ANR 對話框

只有在設備的開發者選項中啓用了顯示所有 ANR 時,Android 纔會針對花費過長時間處理廣播消息的應用顯示 ANR 對話框。我們可以通過打開這個選項,在開發中及早發現相關問題。

TraceView 是 Android SDK 中內置的一個工具,它可以加載 trace 文件,用圖形的形式展示代碼的執行時間、次數及調用棧,便於我們分析。(注意:Android Studio3.2之後已經棄用)

Android Studio3.2之後,CPU Profiler替代了TraceView,我們可以通過在通過記錄應用交互過程獲取相關方法執行順序和耗時圖,從而分析哪些方法耗時過長導致ANR。

2、線上ANR數據收集

在我們日常開發中經常遇到的ANR問題都是線上用戶使用時發現的ANR問題,如果是我們開發中就已經發現,那是非常好解決的,只需要聚焦於新增代碼塊即可,但是如果是針對線上版本,那麼我們就需要對線上數據進行監控,很多公司都會自主研發APM系統,或者是借用第三方的,抑或使用Google官方的,都是可以統計到ANR的數據。其實有時候發現站在巨人的肩膀上去做一些事情,也許效率會更高,以下是一些常用的ANR數據收集工具:

  • 國外

    • Google vitals:Google Play自帶的性能統計工具。
    • Firebase Crashlytics:Google Cloud Platform爲應用開發者們提供的實時性能分析系統。
    • ACRA:在Goolge Play上有2.68%實用率的ACRA庫。
  • 國內

    • Bugly:騰訊Bugly項目組推出移動應用崩潰監控分析平臺,提供崩潰、腳本錯誤、ANR(Android)/卡頓(iOS)問題等監控分析服務。
    • xCrash:愛奇藝開源的一個性能監控的SDK。
    • Umeng:國內移動統計分析服務平臺,提供統計分析、更新、分享、推送等服務,其中,錯誤分析也是在統計分析的基礎上添加。
    • Testin:國內雲測平臺,服務升級後,提供雲測,APM服務(包括性能監控,錯誤上報等),衆測等服務。

三、如何解決問題

無論我們通過線上還是開發中發現了ANR問題,我們都需要思考怎麼去解決。

1、修改主線程上耗時的代碼

通過TraceView或者CPU Profiler可以找到應用中主線程忙碌時間超過5s的位置,然後把此處代碼移到子線程操作。如一些網絡操作、耗時計算等。

2、鎖的競爭導致堵塞

如果主線程參與鎖的競爭,有可能會導致主線程持續等待鎖而造成堵塞的問題,從而引發ANR。所以最好還是避免主線程出現競爭鎖的情況。

3、死鎖

如果某資源被另一個線程所持有,而該線程又在等待主線程的資源,就會陷入死鎖情況導致ANR。

4、廣播接收器執行速度慢

如在廣播接收器的onReceive()執行了長時間的耗時操作,就會可能導致ANR,所以應該把耗時操作放到子線程操作。

四、如何實現ANR監控

站在巨人的肩膀固然好,但是如果能夠自己變成巨人那就更好了,所以作爲一個有追求的開發者,我們當然要思考如何監控APP的ANR問題。

目前流行的ANR檢測方案有開源的BlockCanary 、ANR-WatchDog、SafeLooper,xCrash, 還有根據谷歌原生系統接口監測的方案:FileObserver。

1、BlockCanary

BlockCanary是Android平臺上的一個輕量的,非侵入式的性能監控組件,可以在使用應用的時候檢測主線程上的各種卡頓問題,並可通過組件提供的各種信息分析出原因並進行修復。

  • 原理:

    • (1) 在Looper裏面的loop方法裏面有一個Printer打印日誌,它在每個Message處理的前後被調用,而如果主線程卡住了,就是dispatchMessage裏卡住了。

      public static void loop() {
          // ....
      
          for (;;) {
              // ...
      
              // 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);
              }
      
              // ...
              msg.target.dispatchMessage(msg);
              // ...
      
              if (logging != null) {
                  logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
              }
             // ...
          }
      }
      
    • (2) 每個線程只有一個Looper,而只要重新設置主線程Looper裏面的Printer即可重寫println方法,然後在這裏面進行前後兩次消息處理的時間差,從而計算是否有發生ANR。

      // 創建自定義Printer
      ...
      @Override
      public void println(String x) {
          if (!mStartedPrinting) {
              mStartTimeMillis = System.currentTimeMillis();
              mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
              mStartedPrinting = true;
          } else {
              final long endTime = System.currentTimeMillis();
              mStartedPrinting = false;
              if (isBlock(endTime)) {
                  notifyBlockEvent(endTime);
              }
          }
      }
      
      private boolean isBlock(long endTime) {
          return endTime - mStartTimeMillis > mBlockThresholdMillis;
      }
      ...
      
    • (3)設置自定義Printer到主線程Looper

      Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
      
  • 優點:靈活配置可監控常見APP應用性能也可作爲一部分場景的ANR監測,並且可以準確定位ANR和耗時調用棧。

  • 缺點:

    • (1) 谷歌已經明確標註This must be in a local variable, in case a UI event sets the logger這個looger對象是可以被更改的,已經有開發者遇到在使用WebView時logger被set爲Null導致BlockCanary失效,只能讓BlockCanary在WebView初始化之後調用start。
    • (2) 如果dispatchMessage執行的非常久是無法觸發BlockCanary的邏輯。
    • (3) 在Printer輸出之前,有一段代碼queue.next()也會可能發生ANR,從而造成無法監控到ANR。
    • (4) 無法監控CPU資源緊張造成系統卡頓無法響應的ANR。

2、ANR-WatchDog

ANR-WatchDog是參考Android WatchDog機制,起了個單獨線程向主線程發送一個變量+1的操作,然後休眠一定時間閾值(閾值可自定義,例如5s),休眠過後再判斷變量是否已經+1,如果未完成則ANR告警。在GP上有有2.68%實用率的ACRA庫也推薦使用這種方式。

  • 原理:如下圖所示。

image

  • 優點:
    • (1) 兼容性好,無需適配機型。
    • (2) 無需改動APP邏輯代碼,非侵入性。
    • (3) 性能影響不大。
  • 缺點:
    • (1) 無法保證能捕獲所有ANR,對閾值設置影響捕獲概率。如時間過長,中間發生的ANR則可能被遺漏掉。

3、SafeLooper

SafeLooper是一個第三方開源的庫,主要用於捕獲未知異常,避免出現ANR彈窗,從而在捕獲到異常時獲取ANR信息。

  • 原理:SafeLooper其實就是一個Runnable,啓動後把這個SafeLooper(消息)post到主線程,並且會輪訓主線程的消息隊列,通過反射把新消息進行處理,然後判斷新消息的處理過程是否會出現ANR導致的異常,如果是就將其捕獲,並把異常信息通過uncaughtException回調給用戶進行處理。主要流程如下圖所示。

image

  • 優點:使用AOP思想進行異常捕獲的思想值得借鑑。
  • 缺點:使用反射會影響性能。

4、FileObserver

通過復現ANR場景,可以發現/data/anr文件夾會伴隨ANR發生而變化,所以可以通過監聽/data/anr目錄下是否有文件寫入,如果有的話就認爲發生了ANR,像Bugly對ANR的監控就是用這種方法來實現的。

  • 原理:自定義FileObserver監聽/data/anr目錄下文件是否有新增".tarce"結尾的文件,如果有則認爲發生ANR,並導出trace文件,注意如果當多個APP同時發生ANR,裏面會有多個trace文件,需要對包名時間等進行過濾。
  • 優點:
    • (1) 基於原生接口調用,時機和內容準確。
    • (2) 無性能問題,實現簡單。
  • 缺點:
    • (1) /data/anr目錄可能不會在發生ANR時馬上寫入文件,可能會發生滯後,從而導致收集到的trace文件和線程信息不準確。

5、xCrash

xCrash是愛奇藝開源的一個性能監控SDK,它的實現方案和以上四種不太一樣,它是通過監控系統的信號量的變化,從而確定是否發生了ANR,然後再整理輸出Tombstone文件。由於我對系統信號量了解不多,這裏就貼出官方文檔的原理圖,歡迎瞭解的大神補充。

image

五、相關參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章