ANR 次數 (Views)

概念和 Jetpack Compose 實作

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

向使用者顯示的 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,可以按照本文提供的指南診斷及修正問題。

修正問題

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

主要執行緒上的過慢程式碼

在程式碼中,找出應用程式主執行緒忙碌時間超過 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 架構中的類別可協助將工作移到背景工作執行緒。詳情請參閱「背景工作執行緒」。

主執行緒上的 I/O

造成主要執行緒作業緩慢的常見原因,是在主執行緒執行 I/O 作業,而這也可能導致 ANR。在 Compose 中,開發人員嘗試衍生初始狀態時,經常會意外觸發磁碟讀取 (例如 SharedPreferences 或資料庫呼叫)。

在 UI 層以外執行長時間執行的 I/O 作業。在 ViewModel 中使用 withContext(Dispatchers.IO),或更理想的做法是在資料層中使用存放區。建議您按照上一個章節的說明,將所有 IO 作業都移到背景工作執行緒。

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

鎖定爭用

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

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

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

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

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

不過如果主要執行緒無法繼續執行,就會顯示 BLOCKED 狀態且無法回應事件。Android 裝置監視器顯示的狀態為「Monitor」(監控) 或「Wait」(等待),如圖 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();
      }
  }
}

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

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

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

過慢的廣播接收器

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

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

應用程式在 BroadcastReceiveronReceive 方法內應該只能執行短作業。不過,如果應用程式由於廣播訊息而需要進行更複雜的處理作業,如果工作預計最多需要幾秒鐘,則應將工作延後到 ViewModel (運用 Kotlin 協同程式、範圍和調度器的強大功能)、任何類型的狀態容器,或延後到 WorkManager (如果工作預計需要幾秒鐘以上)。

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

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

圖 5:顯示 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 時間軸顯示,工作已經延後到背景工作執行緒內。

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

圖 6. 顯示在背景工作執行緒處理廣播訊息的 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 逾時情形仍然會發生。