ANR

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

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

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

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

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

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

偵測問題

如果您已發布應用程式,可以使用 Android Vitals 查看應用程式的 ANR 相關資訊。您可以使用其他工具偵測欄位中的 ANR,但請注意,在 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 事件。

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

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

如要瞭解 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 對話方塊。

重新組合瓶頸

使用 Android Studio 分析器版面配置檢查器追蹤重新組合瓶頸。詳情請參閱「Jetpack Compose 效能」。

擷取追蹤檔

發生 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>

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

修正問題

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

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

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

常見問題是在可組合函式中直接執行長時間執行的工作:

@Composable
fun BadList(rawStrings: List<String>) {
    // Math or sorting inside the composable runs on EVERY recomposition pass!
    val heavilyProcessedList = rawStrings
        .filter { it.isNotBlank() }
        .map { it.uppercase().reversed() }
        .map { it.computationallyHeavyFunction() }
.sortedBy { it.length }
    LazyColumn { items(sortedList) { Text(it) } }
}

// Modern Compose-first fix
@Composable
fun GoodList(viewModel: MyViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // UI simply renders state; no heavy processing allowed here
    LazyColumn { items(uiState.sortedData) { Text(it) } }
}

主執行緒上的 I/O

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

在 UI 層以外執行長時間執行的 I/O 作業。在 ViewModel 中使用 withContext(Dispatchers.IO),或是在資料層使用 Repository

死結

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

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

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

使用 Kotlin 和 Compose 時,您可以將原始鎖定替換為非封鎖協同程式互斥鎖 (Mutex.withLock),藉由「暫停」執行環境,而非凍結 UI 執行緒,防止執行緒遭到封鎖。例如:

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

// Modern non-blocking concurrency state architecture
class SecureDataRepository {
    private val mutex = Mutex()

    suspend fun safeUIAccess() {
        // If locked, the main thread suspends seamlessly, preventing an ANR
        mutex.withLock {
            performSafeOperation()
        }
    }
}

過慢的廣播接收器

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

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

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

GameActivity

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

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

其他資源

Views content