ANR

當 Android 應用程式的 UI 執行緒遭封鎖的時間過長,會觸發「應用程式無回應」(ANR) 錯誤。如果應用程式在前景執行,系統會向使用者顯示對話方塊,如圖 1 所示。ANR 對話方塊可讓使用者選擇強制退出應用程式。

圖 1. 向使用者顯示的 ANR 對話方塊

圖 1. 向使用者顯示的 ANR 對話方塊

之所以發生 ANR 是個問題,是因為應用程式是 更新 UI,無法處理使用者輸入事件或繪圖,造成使用者困擾 使用者。如果想進一步瞭解應用程式的主要執行緒,請參閱「程序和 執行緒

當發生以下任一種情形,應用程式就會觸發 ANR:

  • 輸入分派作業逾時:如果應用程式尚未回應輸入值 事件 (例如按鍵或螢幕觸控) 會在 5 秒內發生。
  • 執行服務:如果應用程式宣告的服務無法完成 執行 Service.onCreate()Service.onStartCommand()/Service.onBind()
  • 未呼叫 Service.startForeground():如果應用程式使用 Context.startForegroundService() 即可在前景啟動新服務, 不過服務不會在 5 秒內呼叫 startForeground()
  • 意圖廣播:如果 BroadcastReceiver 未在一定時間內結束執行。如果應用程式在前景有任何活動,逾時時間為 5 秒。
  • JobScheduler 互動:如果 JobService 未在幾秒內從 JobService.onStartJob()JobService.onStopJob() 傳回;或者,如果由使用者發起的工作啟動了,而您的應用程式在呼叫 JobService.onStartJob() 後的幾秒內並未呼叫 JobService.setNotification()。如果應用程式指定 Android 13 以下版本,則 ANR 不會發出通知,也不會回報給應用程式。如果指定 Android 14 以上版本,ANR 會發出通知,也會回報給應用程式。

如果您的應用程式發生 ANR,可以按照本文提供的指南診斷及修正問題。

偵測問題

如果您已經發布應用程式,您可以使用 Android Vitals 可讓你查看應用程式的 ANR 資訊。您可以使用 有一項工具可以偵測領域中的 ANR,但請注意,第三方工具無法回報 舊版 Android (Android 10 以下版本),與 Android Vitals 不同。

Android Vitals

Android Vitals 可協助您監控和改善應用程式的 ANR 發生率。 Android Vitals 會測量多種 ANR 發生率:

  • ANR 發生率:遇到每日活躍使用人數百分比。 發生任何類型的 ANR
  • 使用者感知的 ANR 發生率:每日活躍使用人數百分比 且至少遇到一次使用者感知 ANR 事件。目前只有 系統會將 Input dispatching timed out 類型視為使用者感知。
  • 多次 ANR 發生率:每日活躍使用人數百分比 發生至少兩次 ANR

「每日活躍使用者」是指在一天內透過單一裝置使用應用程式的不重複使用者,可能會執行多個工作階段。如果使用者在一天內透過多部裝置使用應用程式,系統會依裝置數量計算當天的活躍使用者。如果多位使用者當天使用同一部裝置,系統只會計為一位活躍使用者。

使用者感知的 ANR 發生率屬於核心指標,會影響 您的應用程式在 Google Play 上的能見度。這項功能非常重要 計算次數是每次使用者與應用程式互動時發生, 服務中斷

Google Play 針對這項指標定義了兩個不良行為門檻

  • 整體不良行為門檻:至少 0.47% 的每日活躍使用者 則發生在所有裝置型號上
  • 每部裝置不良行為門檻:至少 8% 的每日使用者 則適用於單一裝置型號的使用者感知 ANR 事件。

如果應用程式超出整體不良行為門檻,使用者可能比較不容易在所有裝置上找到該應用程式。如果您的應用程式超出每部裝置的不良行為 某些裝置的使用者行為門檻 這些裝置,您的商店資訊中可能會顯示警告訊息。

Android Vitals 會透過 Play 管理中心 避免發生過多 ANR 事件

如要瞭解 Google Play 如何收集 Android Vitals 資料,請參閱 Play 管理中心說明文件。

診斷 ANR

以下是 ANR 常見的模式,可供您在診斷時參考:

  • 應用程式在主執行緒進行含有 I/O 的作業時速度過慢。
  • 應用程式在主執行緒進行長時間的運算。
  • 主要執行緒向另一個程序發出同步繫結呼叫;且 延遲時間也很長
  • 主要執行緒因等待同步區塊長時間而遭到封鎖 作業中,作業會在其他執行緒上進行。
  • 主要執行緒與另一個執行緒形成死結, 或透過 Binder 呼叫來處理。主要執行緒不只要等待太久 作業結束,但會形成死結。如需更多資訊 請參閱維基百科上的「死結」。

以下技巧可以幫助您判斷 ANR 的發生原因。

HealthStats

HealthStats 會擷取各項總計資料,包括使用者和系統時間、CPU 作業時間、網路、無線電統計資料、螢幕開啟/關閉時間和喚醒時間,藉此提供應用程式運作狀態的指標。這可以 有助於評估整體 CPU 使用率與電池耗電

偵錯

Debug 有助於檢查 Android 應用程式 包括透過追蹤和配置計數來辨識卡頓情形 效能和差不多您還可以使用 Debug 取得執行階段和原生記憶體計數器,以及有助於識別特定程序記憶體用量的記憶體指標。

ApplicationExitInfo

ApplicationExitInfo 可以使用 Android 11 (API 級別 30) 以上版本,並提供 造成應用程式結束的原因包括 ANR、記憶體不足、應用程式當機 CPU 用量過多、使用者中斷、系統中斷或執行階段 權限變更。

嚴格模式

使用 StrictMode 可幫助你找到 開發應用程式時,主執行緒意外發生 I/O 作業。 您可以使用 StrictMode 在應用程式或活動層級提供資源

啟用背景 ANR 對話方塊

只有在裝置的「開發人員選項」中已啟用「Show all ANRs」的情況下,Android 才會在應用程式處理廣播訊息時間過長時,顯示 ANR 對話方塊。因此,使用者不一定會看到背景 ANR 對話方塊,但應用程式依然可能發生效能問題。

Traceview

您可以在應用程式執行各種用途時,使用 Traceview 取得追蹤記錄,並找出主執行緒忙碌的位置。如要瞭解如何使用 Traceview,請參閱「使用 Traceview 和 dmtracedump 剖析」。

提取追蹤檔

發生 ANR 時,Android 會儲存追蹤資訊。在較舊的 OS 版本中,裝置上會有一個 /data/anr/traces.txt 檔案。在較新的 OS 版本中,則會有多個 /data/anr/anr_* 檔案。您可以使用 Android Debug Bridge (adb) 做為根層級,存取裝置或模擬器中的 ANR 追蹤記錄:

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

您可以使用裝置上的「Take bug report」開發人員選項,或在開發機器上使用 adb bugreport 指令,在實體裝置上擷取錯誤報告。詳情請參閱「擷取及查看錯誤報告」。

修正問題

找出問題後,您可以使用本章節提到的方法修復常見問題。

主執行緒上的過慢程式碼

在程式碼中,找出應用程式主執行緒忙碌時間超過 5 秒的部分。請尋找應用程式中可疑的用途,並嘗試重現 ANR。

舉例來說,圖 2 的 Traceview 時間軸顯示了主執行緒忙碌時間超過 5 秒的部分。

圖 2. 顯示主執行緒忙碌狀態的 Traceview 時間軸

圖 2. 顯示主執行緒忙碌狀態的 Traceview 時間軸

圖 2. 顯示多數會造成問題的程式碼 onClick(View) 處理常式,如以下程式碼範例所示:

Kotlin

override fun onClick(v: View) {
    // This task runs on the main thread.
    BubbleSort.sort(data)
}

Java

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

在這種情況下,您應將主執行緒中執行的工作移至背景工作執行緒。Android 架構中的類別可協助將工作移到背景工作執行緒。詳情請參閱「背景工作執行緒」。

主執行緒上的 IO

造成主要執行緒作業緩慢的常見原因,是在主執行緒執行 IO 作業,而這也可能導致 ANR。建議您按照上一個章節的說明,將所有 IO 作業都移到背景工作執行緒。

IO 作業的例子包括網路和儲存空間作業。詳情請參閱「執行網路作業」和「儲存資料」。

鎖定爭用

在某些情況下,造成 ANR 的工作並非直接在應用程式的主執行緒上執行。如果背景工作執行緒持有某個資源的鎖,而主執行緒需要這個資源才能完成工作,就可能發生 ANR。

舉例來說,圖 3 的 Traceview 時間軸顯示,多數工作都是 對背景工作執行緒執行的動作

圖 3. TraceView 時間軸顯示工作在背景工作執行緒中執行

圖 3. TraceView 時間軸顯示工作在背景工作執行緒中執行

不過,如果使用者依然會碰到 ANR,建議您查看 Android Device Monitor 中的主執行緒一般來說,主要執行緒會位於 RUNNABLE 的狀態 (如果已經準備好更新 UI 並一般回應)。

不過,如果主執行緒無法繼續執行作業,則訊息會處於 BLOCKED 狀態 但無法回應活動Android 裝置監視器顯示的狀態為 監控等待,如圖 5 所示。

圖 4. 處於 Monitor 狀態的主執行緒

圖 4. 處於 Monitor 狀態的主執行緒

以下追蹤記錄顯示應用程式的主執行緒因等待資源而遭到封鎖:

...
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)
...

查看追蹤記錄可幫助找出封鎖主執行緒的程式碼。在先前的追蹤記錄中,以下程式碼為主執行緒遭封鎖的原因:

Kotlin

override fun onClick(v: View) {
    // The worker thread holds a lock on lockedResource
    LockTask().execute(data)

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

class LockTask : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? =
            synchronized(lockedResource) {
                // This is a long-running operation, which makes
                // the lock last for a long time
                BubbleSort.sort(params[0])
            }
}

Java

@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]);
       }
   }
}

另一個例子是應用程式的主執行緒正在等待背景工作執行緒的結果,如以下程式碼所示。請注意,我們不建議在 Kotlin 中使用 wait()notify(),因為 Kotlin 本身具有處理並行情況的機制。使用 Kotlin 時,請盡可能使用 Kotlin 專屬的機制。

Kotlin

fun onClick(v: View) {
    val lock = java.lang.Object()
    val waitTask = WaitTask(lock)
    synchronized(lock) {
        try {
            waitTask.execute(data)
            // Wait for this worker thread’s notification
            lock.wait()
        } catch (e: InterruptedException) {
        }
    }
}

internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        synchronized(lock) {
            BubbleSort.sort(params[0])
            // Finished, notify the main thread
            lock.notify()
        }
    }
}

Java

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、 以及資源集區 (例如資料庫連線集區) 或其他互斥 (互斥鎖) 機制。

一般而言,您應該評估應用程式會鎖定資源。 為了避免發生 ANR,那麼應該檢查資源受到鎖定 主執行緒所需的資源

確定鎖定時間不多,甚至更棒 請先評估應用程式是否需要保留這項保留狀態。如果您需要用鎖定的方式,才能根據背景工作執行緒的處理工作決定更新 UI 的時機,請用 onProgressUpdate()onPostExecute() 這類機制,讓背景工作執行緒和主執行緒彼此通訊。

死結

當執行緒因需要要求而進入等待狀態時,就會發生死結情形 其他執行緒持有資源,但這些執行緒也正在等待由 第一個執行緒。如果應用程式的主執行緒有這種情形,就可能發生 ANR。

死結現象已在電腦科學領域中經過充分研究,目前已有演算法可用於防範這項問題。

詳情請參閱維基百科上的「死結」和「死結防範演算法」內容。

過慢的廣播接收器

應用程式可透過廣播接收器回應廣播訊息,例如啟用/停用飛航模式或變更連線狀態。當應用程式處理廣播訊息的時間過長,就會發生 ANR。

當有以下情形時,就會發生 ANR:

您的應用程式應該只在 onReceive() 方法 BroadcastReceiver. 不過,如果您的應用程式需要 您應將工作延後到 IntentService.

您可以使用 Traceview 等工具,確認廣播接收器是否在應用程式主執行緒上進行需長時間執行的作業。舉例來說,圖 6 的時間軸顯示,廣播接收器在主執行緒上處理訊息的時間約為 100 秒。

圖 5. 顯示 `BroadcastReceiver` 在主要元件的 Traceview 時間軸
討論串

圖 5. 顯示 BroadcastReceiver 於 主執行緒

之所以出現這項行為,可能是因為對 onReceive() 方法 BroadcastReceiver、 如以下範例所示:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    // This is a long-running operation
    BubbleSort.sort(data)
}

Java

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

在這種情況下,建議您把長時間執行的作業移到 IntentService,因為它會使用背景工作執行緒執行工作。以下程式碼顯示如何使用 IntentService 來處理 長時間執行的作業:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    Intent(context, MyIntentService::class.java).also { intentService ->
        // The task now runs on a worker thread.
        context.startService(intentService)
    }
}

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        BubbleSort.sort(data)
    }
}

Java

@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,長時間執行的作業現在已在背景工作執行緒執行,而非主執行緒。圖 7 的 Traceview 時間軸顯示,工作已經延後到背景工作執行緒內。

圖 6. 顯示在背景工作執行緒處理廣播訊息的 TraceView 時間軸

圖 6. 顯示在背景工作執行緒處理廣播訊息的 TraceView 時間軸

廣播接收器可以使用 goAsync() 告知系統需要更多時間才能處理訊息。不過 你應該打電話。 finish()PendingResult 物件。以下範例顯示如何呼叫 finish() 讓系統能夠讓系統 回收廣播接收器,避免發生 ANR:

Kotlin

val pendingResult = goAsync()

object : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        // This is a long-running operation
        BubbleSort.sort(params[0])
        pendingResult.finish()
        return 0L
    }
}.execute(data)

Java

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 逾時情形仍然會發生。

GameActivity

根據個案研究,在以 C 或 C++ 編寫的遊戲和應用程式中,GameActivity 程式庫可減少 ANR 情形。如果您將現有原生活動替換為 GameActivity,可以減少 UI 執行緒封鎖情況,並防止某些 ANR 發生。

如要進一步瞭解 ANR,請參閱「讓應用程式維持正常回應速度」。如果想進一步瞭解執行緒,請參閱「執行緒作業效能」。