ANR

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

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

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

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

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

  • 活動在前景執行時,應用程式未在 5 秒內回應輸入事件或 BroadcastReceiver (例如按鍵或螢幕觸控事件)。
  • 前景並未執行任何活動,而 BroadcastReceiver 已經長時間無法結束執行作業。

如果您的應用程式發生 ANR,請按照本文列出的指南診斷及修正問題。

偵測及診斷問題

Android 提供多種可以讓您得知應用程式出現問題的方法,並可以幫助您診斷問題原因。如果您已經發布應用程式,Android Vitals 可以在發生問題時警告您,也有提供可以幫助您找出問題來源的診斷工具。

Android Vitals

Android Vitals 會透過 Play 管理中心在應用程式發生過多次 ANR 情形時發出提醒,藉此改善應用程式的效能。應用程式出現下列情況時,Android Vitals 會考慮 ANR 次數過多:

  • 顯示在至少 0.47% 的每日工作階段中發生至少一次 ANR。
  • 顯示在至少 0.24% 的每日工作階段中發生兩次以上的 ANR。

「每日工作階段」是指使用者一天內使用應用程式的時間。

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

診斷 ANR

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

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

以下技巧可以幫助您找到 ANR 發生原因是以上哪一種。

嚴格模式

使用 StrictMode 可以幫助您在應用程式開發期間找出主要執行緒上意外發生的 I/O 作業。您可以在應用程式或活動層級使用 StrictMode

啟用背景 ANR 對話方塊

只有在裝置的「開發人員選項」中已經啟用「Show all ANRs」(顯示所有 ANR) 的情況下,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 逾時。

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