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 及以下版本) 上第三方工具無法回報 ANR 情形,這點與 Android Vitals 不同。

Android Vitals

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

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

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

使用者感知的 ANR 發生率屬於核心指標,會影響應用程式在 Google Play 上的曝光度。這項指標非常重要,因為計入指標的 ANR 情形皆是在使用者與應用程式互動時發生,嚴重影響了使用者體驗。

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

  • 整體裝置不良行為門檻:在所有裝置型號上,至少有 0.47% 的每日活躍使用者感知到 ANR 情形。
  • 每部裝置不良行為門檻:單一裝置型號上,至少 8% 的每日活躍使用者感知到 ANR 情形。

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

Android Vitals 會透過 Play 管理中心在應用程式發生太多次 ANR 時發出提醒。

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

診斷 ANR

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

  • 應用程式在主要執行緒進行含有 I/O 的作業時速度過慢。
  • 應用程式在主要執行緒進行長時間的運算。
  • 主要執行緒向另一個程序發出同步繫結呼叫,而該程序回傳花費的時間過長。
  • 由於等待其他執行緒上進行長時間作業的同步區塊,導致主要執行緒遭到封鎖。
  • 主要執行緒在程序中或透過繫結呼叫和另一個執行緒形成死結。主要執行緒不只要等待長時間作業完成,並會形成死結。詳情請參閱維基百科的「死結」。

以下技巧可以幫助您判斷 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。

舉例來說,圖 4 的 Traceview 時間軸顯示多數工作都是在背景工作執行緒中執行。

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

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

不過,如果使用者依然會碰到 ANR,那麼您應該用 Android 裝置監視器檢查主要執行緒的狀態。一般來說,如果主要執行緒已經準備好更新 UI 並可以正常回應,就會顯示 RUNNABLE 狀態。

不過如果主要執行緒無法繼續執行,就會顯示 BLOCKED 狀態且無法回應事件。Android 裝置監視器顯示的狀態為「Monitor」(監控) 或「Wait」(等待),如圖 5 所示。

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

圖 5. 處於 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();
       }
   }
}

也有其他的情形可能會封鎖主要執行緒,包括使用 LockSemaphore,以及資源集區 (如資料庫連線集區) 的執行緒,或其他互斥 (互斥鎖) 機制。

一般而言,您應該評估應用程式會鎖定資源的內容,但是如果您想避免發生 ANR,就應該檢查會鎖定主要執行緒所需資源的內容。

確定鎖定時間可以降到最低,更好的做法則是評估應用程式究竟需不需要保留這項鎖定。如果您需要用鎖定的方式,才能根據工作站執行緒的處理工作決定更新 UI 的時機,請用 onProgressUpdate()onPostExecute() 這類機制,以便讓工作站和主要執行緒彼此溝通。

死結

當某執行緒需要的資源被另一個執行緒佔據而必須進入等待狀態,但是這另一個執行緒也正在等待第一個執行緒佔據的資源時,就會發生死結。如果應用程式的主要執行緒發生這種情形,就可能發生 ANR。

在電腦科學領域,死結是經常研究的現象,您也可以使用防止死結的演算法避免發生死結。

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

過慢的廣播接收器

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

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

應用程式在 BroadcastReceiveronReceive() 方法內應該只能執行短作業。不過,如果如果應用程式由於廣播訊息而需要進行更複雜的處理作業,應該將工作延後到 IntentService

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

圖 6. 顯示 BroadcastReceiver 在主要執行緒作業的 Traceview 時間軸

圖 6. 顯示 BroadcastReceiver 在主要執行緒作業的 Traceview 時間軸

之所以出現這項行為,可能是在 BroadcastReceiveronReceive() 方法上進行長時間執行的作業所致,如以下範例所示:

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 時間軸顯示工作已經延後到工作站執行緒內。

圖 7. Traceview 時間軸顯示了在工作站執行緒上處理的廣播訊息

圖 7. 顯示在工作站執行緒上處理的廣播訊息的 Traceview 時間軸

廣播接收器可以利用 goAsync() 告知系統需要更多時間才能處理訊息。不過,您應該在 PendingResult 物件呼叫 finish()。以下範例說明了如何藉由呼叫 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 更進一步的資訊請參閱「讓應用程式維持正常回應速度」。如果想進一步瞭解執行緒,請參閱「執行緒作業效能」。