Android開發(fā)套路# ANR(應用程序不響應)

當Android應用程序的UI線程阻塞時間過長時,會觸發(fā)“應用程序不響應”(ANR)錯誤。如果應用程序處于前臺,系統(tǒng)會向用戶顯示一個對話框,如圖1所示。ANR對話框為用戶提供強制退出應用程序的機會。


圖1.顯示給用戶的ANR對話框.png

ANR是一個問題,因為應用程序的主線程(負責更新UI)不能處理用戶輸入的事件或繪制,從而導致用戶感到沮喪。

當下列情況之一發(fā)生時,您的應用將觸發(fā)ANR:
  • 當你的活動處于前臺時,你的應用程序BroadcastReceiver在5秒內(nèi)沒有響應輸入事件或(例如按鍵或屏幕觸摸事件)。
  • 雖然在前臺沒有任何活動,但BroadcastReceiver在相當長的時間內(nèi)還沒有完成執(zhí)行。
  • 如果你的應用程序遇到ANR,則可以使用本文中的指導來診斷和解決問題。

檢測和診斷問題

Android提供了多種方式讓您知道你的應用有問題,并幫助你診斷。如果你已經(jīng)發(fā)布了你的應用程序,Android維權人員可以提醒你問題正在發(fā)生,并且有診斷工具可幫助你查找問題。

Android的重要功能

Android的重要功能可以通過Play控制臺提醒你,當你的應用顯示過多的ANR時,可以幫助你提高應用的性能 。Android的重要功能認為應用程序的ANR過度:

  • 至少在每日會議的0.47%中至少展示一次ANR。
  • 展品2或更多的ANR至少在其日常會議的0.24%。
  • 一個日常的會話是指在使用你的應用程序的日子。

診斷ANRs

在診斷ANR時有一些常見的模式可供選擇:

  • 該應用程序正在執(zhí)行緩慢的操作涉及主線程上的I / O。
  • 該應用程序在主線程上做了長時間的計算。
  • 主線程正在對另一個進程執(zhí)行同步聯(lián)編程序調(diào)用,而另一個進程需要很長時間才能返回。
  • 主線程被阻塞,等待一個正在另一個線程上發(fā)生的長操作的同步塊。
  • 主線程與另一個線程處于死鎖狀態(tài),無論是在你的進程還是通過聯(lián)編程序調(diào)用。主線不僅僅是等待長時間的操作才能完成,而是陷入了僵局。

以下技術可以幫助您找出哪些原因?qū)е履腁NR。

strict模式

使用StrictMode幫助你在開發(fā)應用程序時在主線程上發(fā)現(xiàn)偶然的I / O操作。你可以StrictMode在應用程序或活動級別使用。

啟用背景ANR對話框

Android僅在設備的“ 開發(fā)人員”選項中顯示所有ANR的情況下,才會顯示ANR對話框,用于處理廣播消息所需的時間太長的應用程序。出于這個原因,背景ANR對話并不總是顯示給用戶,但應用程序仍然可能遇到性能問題。

Traceview

您可以使用Traceview在遍歷用例的同時獲取正在運行的應用程序的跟蹤信息,并確定主線程繁忙的地方。

使用一個跟蹤文件

Android /data/anr/traces.txt在設備上的文件中遇到ANR時會存儲一些跟蹤信息 。你可以通過以root用戶身份在設備上啟動shell會話來從仿真程序獲取文件,如以下命令行示例所示:

adb root
adb shell
cat /data/anr/traces.txt

你可以使用設備上的“獲取錯誤報告開發(fā)者”選項或開發(fā)計算機上的“adb bugreport”命令來從物理設備捕獲錯誤報告。

解決問題

在確定問題后,你可以使用本問文中的提示來解決常見問題。

主線程上的代碼變慢

確定你的代碼中應用程序的主線程忙于超過5秒鐘的地方。在你的應用程序中查找可疑用例并嘗試重現(xiàn)ANR。

例如,圖2顯示了Traceview時間線,其中主線程忙于超過5秒鐘。


圖2. Traceview時間線顯示繁忙的主線程.png

圖2顯示了大多數(shù)有問題的代碼發(fā)生在onClick(View)處理程序中,如以下代碼示例所示:

@Override
public void onClick(View view) {
   // This task runs on the main thread.
   BubbleSort.sort(data);
}

在這種情況下,你應該將在主線程中運行的工作移至工作線程。Android框架包括可以幫助將任務移動到工作線程的類,以下代碼顯示了如何使用AsyncTaskhelper類來處理工作線程上的任務:

@Override
public void onClick(View view) {
   // The long-running operation is run on a worker thread
   new AsyncTask<Integer[], Integer, Long>() {
       @Override
       protected Long doInBackground(Integer[]... params) {
           BubbleSort.sort(params[0]);
       }
   }.execute(data);
}

Traceview顯示大部分代碼在工作線程上運行,如圖3所示。主線程可用于響應用戶事件。


圖3. Traceview時間線顯示工作線程處理的工作.png

主線程上的IO

在主線程上執(zhí)行IO操作是導致主線程操作速度慢的一個常見原因,這會導致ANR。建議將所有IO操作移至工作線程。
IO操作的一些示例是網(wǎng)絡和存儲操作。

爭用鎖

在某些情況下,導致ANR的工作不會直接在應用程序的主線程上執(zhí)行。如果工作者線程在主線程完成工作所需的資源上持有鎖,則可能會發(fā)生ANR。

例如,圖4顯示了Traceview時間線,其中大部分工作是在工作線程上執(zhí)行的。


圖4.顯示正在工作線程上執(zhí)行的工作的Traceview時間線.png

但是,如果你的用戶仍然遇到ANR,則應該查看Android設備監(jiān)視器中主線程的狀態(tài)。通常情況下,如果主線程已經(jīng)RUNNABLE準備好更新用戶界面,并且通常是響應的,那么主線程就處 但如果主線程無法恢復執(zhí)行,則處于BLOCKED狀態(tài),無法響應事件。Android設備監(jiān)視器上的狀態(tài)顯示為“ 監(jiān)視”或“ 等待”,如圖5所示。


圖5.監(jiān)視器狀態(tài)中的主線程.png

以下跟蹤顯示了被阻止等待資源的應用程序主線程:

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

查看跟蹤可以幫助你找到阻塞主線程的代碼。下面的代碼負責持有阻塞前一個跟蹤中的主線程的鎖:

@Override
public void onClick(View v) {
    // The worker thread holds a lock on lockedResource
   new LockTask().execute(data);

   synchronized (lockedResource) {
       // The main thread requires lockedResource here
       // but it has to wait until LockTask finishes using it.
   }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (lockedResource) {
           // This is a long-running operation, which makes
           // the lock last for a long time
           BubbleSort.sort(params[0]);
       }
   }
}

另一個示例是應用程序的主線程正在等待工作線程的結(jié)果,如以下代碼所示:

de
public void onClick(View v) {
   WaitTask waitTask = new WaitTask();
   synchronized (waitTask) {
       try {
           waitTask.execute(data);
           // Wait for this worker thread’s notification
           waitTask.wait();
       } catch (InterruptedException e) {}
   }
}

class WaitTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (this) {
           BubbleSort.sort(params[0]);
           // Finished, notify the main thread
           notify();
       }
   }
}

還有一些其他的情況下,可以阻止主線程,包括使用線程Lock,Semaphore以及資源池(如數(shù)據(jù)庫連接池)或其他互斥(互斥)的機制。

一般來說,你應該評估你的應用程序?qū)Y源所持有的鎖,但是如果你想避免ANR,那么你應該看看為主線程所需的資源所持有的鎖。

確保鎖保持最少的時間,或者甚至更好,評估應用程序是否需要首先舉行。如果使用鎖來確定何時基于工作線程的處理來更新UI,請使用諸如onProgressUpdate()和之類的機制onPostExecute()來在工作線程和主線程之間進行通信。

死鎖

當線程進入等待狀態(tài)時會發(fā)生死鎖,因為所需的資源由另一個線程持有,這也正在等待第一個線程占用的資源。如果應用程序的主線程在這種情況下,ANR可能會發(fā)生。

死鎖在計算機科學中是一個研究得很好的現(xiàn)象,并且有可以用來避免死鎖的死鎖預防算法。

慢速廣播接收機

應用程序可以通過廣播接收器響應廣播消息,例如啟用或禁用飛行模式或更改連接狀態(tài)。當應用程序花費太長時間來處理廣播消息時,會發(fā)生ANR。

ANR發(fā)生在下列情況下:

  • 廣播接收機還沒有onReceive()在相當長的時間內(nèi)完成其方法。
  • 廣播接收者呼叫goAsync()并且不能呼叫finish()PendingResult對象。
  • 你的應用程序只能在onReceive()方法中執(zhí)行短操作BroadcastReceiver。但是,如果你的應用程序由于廣播消息而需要更復雜的處理,則應將任務推遲到IntentService

你可以使用Traceview等工具來確定你的廣播接收器是否在應用的主線程上執(zhí)行長時間運行的操作。例如,圖6顯示了在主線程上處理消息大約100秒的廣播接收器的時間線。

圖6.顯示```BroadcastReceiver```在主線程上工作的```Traceview```時間線.png

此行為可以通過對該onReceive()方法執(zhí)行長時間運行的操作來引起BroadcastReceiver,如以下示例所示:

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

在這樣的情況下,建議將長時間運行的操作移動到一個IntentService,因為它使用一個工作線程執(zhí)行工作。以下代碼顯示如何使用一個IntentService來處理長時間運行的操作:

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
   @Override
   protected void onHandleIntent(@Nullable Intent intent) {
       BubbleSort.sort(data);
   }
}

IntentService在工作者線程而不是主線程上執(zhí)行長時間運行的操作。圖7顯示了推遲到Traceview時間線中的工作線程的工作。

圖7. Traceview時間線顯示在工作線程上處理的廣播消息.png

你的廣播接收器可以用goAsync()來向系統(tǒng)發(fā)出信號表示它需要更多時間來處理消息。但是,你應該調(diào)用finish()的PendingResult對象。以下示例顯示如何調(diào)用finish()從而讓系統(tǒng)回收廣播接收器并避免ANR

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
   @Override
   protected Long doInBackground(Integer[]... params) {
       // This is a long-running operation
       BubbleSort.sort(params[0]);
       pendingResult.finish();
   }
}.execute(data);

但是,如果廣播在后臺,將代碼從慢速廣播接收器移動到另一個線程并使用goAsync()將不會修復ANR。ANR超時仍然適用。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容