當機

Android 應用程式會在發生未處理的例外狀況或信號導致的意外結束時,出現當機。如果是使用 Java 或 Kotlin 編寫的應用程式,則會在擲回 Throwable 類別所代表的未處理例外狀況時當機。至於使用機器碼或 C++ 撰寫的應用程式,如果在執行期間遇到未處理的信號 (例如 SIGSEGV),也會當機。

應用程式當機時,Android 會終止應用程式的程序並顯示對話方塊,讓使用者知道應用程式已停止運作,如圖 1 所示。

Android 裝置上的應用程式當機問題

圖 1 Android 裝置上的應用程式當機問題

應用程式即使不在前景執行,也會出現當機。任何應用程式元件,即使是在背景執行的廣播接收器或內容供應器等元件,都可能導致應用程式當機。這類當機問題經常會讓使用者感到困惑,因為他們並未主動與應用程式進行互動。

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

偵測問題

您不一定每次都會發現使用者在使用應用程式時遇到當機問題。如果您已發布應用程式,Android Vitals 可以協助您查看應用程式的當機率。

Android Vitals

Android Vitals 可協助您監控及改善應用程式的當機率。這項功能會測量多種當機率:

  • 當機率:遇到任何當機類型的每日活躍使用者百分比。
  • 使用者感知的當機率:在主動使用應用程式時遇到至少一次當機的每日活躍使用者百分比。如果應用程式顯示任何活動或執行任何前景服務,系統會將應用程式視為處於主動使用狀態。

  • 多次當機率:遇到至少兩次當機的每日活躍使用者百分比。

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

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

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

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

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

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

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

診斷當機事件

確定應用程式會回報當機問題後,下一步就是診斷問題。解決當機問題並不容易。不過,如果您能找出造成當機的根本原因,那麼您也許可以找到因應方法。

有許多情況會導致應用程式發生當機。有些原因比較明顯,例如檢查空值或空白字串,但有些較為輕微,例如將無效引數傳送至 API,甚至是複雜的多執行緒互動。

每當 Android 發生當機時,系統就會產生堆疊追蹤,也就是當機之前程式呼叫一系列巢狀函式的數據匯報。您可以在 Android Vitals 中查看當機堆疊追蹤。

如何解讀堆疊追蹤

修正當機問題的第一步是找出發生位置。如果您使用 Play 管理中心或 logcat 工具的輸出內容,則可使用報表詳細資料中提供的堆疊追蹤記錄。如果您沒有可用的堆疊追蹤記錄,則請手動測試應用程式,或與受影響的使用者聯絡,進而在本機重現當機,然後在使用 Logcat 時重現當機。

下列追蹤記錄是以 Java 程式設計語言編寫的應用程式當機情況示例:

--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException: crash sample
at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
at android.view.View.performClick(View.java:6134)
at android.view.View$PerformClick.run(View.java:23965)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6440)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)
--------- beginning of system

堆疊追蹤記錄會顯示兩項與偵錯當機問題相關的重要資訊:

  • 擲回的例外狀況類型。
  • 擲回例外狀況的程式碼區段。

擲回的例外狀況類型通常是指出問題所在的重要提示。請查看例外狀況是否是 IOExceptionOutOfMemoryError 或其他類別,然後找到該例外狀況類別的說明文件。

擲回例外狀況的來源檔案的類別、方法、檔案和行數,會顯示在堆疊追蹤記錄的第二行。在每個呼叫的函式中,另一行都會顯示前面的呼叫網站 (也稱為堆疊框架)。逐步操作堆疊並檢查程式碼,即可找出傳送不正確值的位置。如果您的程式碼未顯示在堆疊追蹤記錄中,可能是因為您在某個位置傳送了無效參數給非同步作業。您通常可以透過檢查堆疊追蹤記錄的每一行,找出您使用的 API 類別,並確認您傳送的參數正確無誤,且您是從允許的位置呼叫,從而弄清楚發生的情況。

以 C 和 C ++ 程式碼編寫的應用程式的堆疊追蹤運作方式大致相同。

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
Timestamp: 2020-02-16 11:16:31+0100
pid: 8288, tid: 8288, name: com.example.testapp  >>> com.example.testapp <<<
uid: 1010332
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
    x0  0000007da81396c0  x1  0000007fc91522d4  x2  0000000000000001  x3  000000000000206e
    x4  0000007da8087000  x5  0000007fc9152310  x6  0000007d209c6c68  x7  0000007da8087000
    x8  0000000000000000  x9  0000007cba01b660  x10 0000000000430000  x11 0000007d80000000
    x12 0000000000000060  x13 0000000023fafc10  x14 0000000000000006  x15 ffffffffffffffff
    x16 0000007cba01b618  x17 0000007da44c88c0  x18 0000007da943c000  x19 0000007da8087000
    x20 0000000000000000  x21 0000007da8087000  x22 0000007fc9152540  x23 0000007d17982d6b
    x24 0000000000000004  x25 0000007da823c020  x26 0000007da80870b0  x27 0000000000000001
    x28 0000007fc91522d0  x29 0000007fc91522a0
    sp  0000007fc9152290  lr  0000007d22d4e354  pc  0000007cba01b640

backtrace:
  #00  pc 0000000000042f89  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::Crasher::crash() const)
  #01  pc 0000000000000640  /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::runCrashThread())
  #02  pc 0000000000065a3b  /system/lib/libc.so (__pthread_start(void*))
  #03  pc 000000000001e4fd  /system/lib/libc.so (__start_thread)

如果原生堆疊追蹤記錄未顯示類別和函式層級資訊,您可能需要產生原生偵錯符號檔案,並將其上傳到 Google Play 管理中心。詳情請參閱「針對當機時的堆疊追蹤去模糊化」。如需原生程式碼發生錯誤的一般資訊,請參閱「診斷原生程式碼發生錯誤的問題」。

重現當機的提示

在某些情況下,僅透過啟動模擬器或將裝置連線至電腦,可能無法完全重現問題。開發環境可以提供更多資源,例如頻寬、記憶體和儲存空間。使用例外狀況類型可確定稀缺資源是什麼,或是找出 Android 版本、裝置類型或應用程式版本之間的關係。

記憶體錯誤

如果您擁有 OutOfMemoryError,則可以建立記憶體不足的模擬器進行測試。圖 2 顯示了 AVD Manager 設定,可讓您控管裝置的記憶體量。

AVD Manager 上的記憶體設定

圖 2. AVD Manager 上的記憶體設定

網路例外狀況

由於使用者經常移入/移出行動網路或 Wi-Fi 網路,因此應用程式的網路例外狀況通常不應被視為「錯誤」,而應視為意外發生的正常作業條件。

如果需要重現網路例外狀況 (例如 UnknownHostException),請在應用程式嘗試使用網路時開啟飛航模式。

或者,選擇網路速度模擬和/或網路延遲,藉此降低模擬器中的網路品質。您可以使用 AVD Manager 的「Speed」和「Latency」設定,也可以使用 -netdelay-netspeed 旗標啟動模擬器,如以下指令列範例所示:

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

這個範例會將所有網路要求的延遲時間設為 20 秒,上傳和下載速度則是 14.4 Kbps。如要進一步瞭解模擬器的指令列選項,請參閱「透過指令列啟動模擬器」。

使用 Logcat 讀取

在您能夠重現當機問題後,就能使用 logcat 等工具取得更多資訊。

Logcat 輸出將顯示您列印的其他記錄訊息,以及系統中的其他內容。別忘了關閉您新增的任何其他 Log 陳述式,因為系統在應用程式執行期間列印這些資訊會浪費 CPU 和電池。

避免因空值指標例外狀況而造成的當機問題

嘗試存取包含空值的物件 (通常是透過叫用其方法或存取其成員) 時發生空值指標例外狀況 (以執行階段錯誤類型 NullPointerException 識別)。空值指標例外狀況是 Google Play 上應用程式當機的主要原因。 空值的用途是指出物件缺失,例如物件尚未建立或指派。為了避免空值指標例外狀況,請先確認您使用的物件參照為非空值,再對其呼叫方法,或嘗試存取其成員。如果物件參照為空值,請妥善處理這個狀況 (例如先結束方法,再對物件參照執行任何作業,並將資訊寫入偵錯記錄檔)。

您不想為每個呼叫方法的每個參數執行空值檢查,因此您可以使用 IDE 或物件的類型來代表是否可為空值。

Java 程式設計語言

下列各節適用於 Java 程式設計語言。

編譯時間警告

使用 @Nullable@NonNull 對方法的參數進行註解並回傳值,以便接收 IDE 的編譯時間警告。這些警告會提示您預期的空值物件:

空值指標例外狀況警告

這些空值檢查適用於已知為空值的物件。@NonNull 物件的例外狀況表明程式碼中存在需要處理的錯誤。

編譯時間錯誤

是否可為空值應是有意義的,因此您可以將其嵌入您使用的類型,以便在編譯時間檢查是否為空值。如果已知物件可以為空值,且應處理是否可為空值問題,則可以將其納入類似 Optional 的物件中。建議您一律選擇傳送是否可為空值的類型。

Kotlin

在 Kotlin 中,是否可為空值是類型系統的一部分。舉例來說,某個變數必須從頭開始宣告為可為空值或不可為空值。可為空值的類型會標示 ?

// non-null
var s: String = "Hello"

// null
var s: String? = "Hello"

不可為空值的變數無法指派有空值,而可為空值的變數在用來做為非空值之前需要檢查其是否可為空值。

如果您不想要明確檢查空值,則可以使用 ?. 安全呼叫運算子:

val length: Int? = string?.length  // length is a nullable int
                                   // if string is null, then length is null

最佳做法是,確保解決可為空值物件的空值情況,否則應用程式可能會進入非預期的狀態。如果您的應用程式不會因為 NullPointerException 再次當機,您將無法得知存在這些錯誤。

下列幾種方法可用來檢查空值:

  • if 檢查

    val length = if(string != null) string.length else 0
    

    基於智慧型轉換和空值檢查功能,Kotlin 編譯器知道字串值是非空值,因此您可以直接使用參照,不需要使用安全呼叫運算子。

  • ?: Elvis 運算子

    這個運算子可讓您指定「如果物件為非空值,則回傳物件;否則回傳其他內容」。

    val length = string?.length ?: 0
    

您還是可以取得以 Kotlin 編寫的 NullPointerException。最常見的情況如下:

  • 明確擲回 NullPointerException 時。
  • 使用 null assertion !! 運算子時。這個運算子會將任何值轉換為非空值類型。如果值為空值,則擲回 NullPointerException
  • 存取平台類型的空值參照時。

平台類型

平台類型為來自 Java 的物件宣告。這些類型經過特殊處理;並非強制進行空值檢查,因此非空值保證與 Java 相同。存取平台類型參照時,Kotlin 不會產生編譯時間錯誤,但這些參照會導致執行階段錯誤。請參考 Kotlin 說明文件中的以下範例:

val list = ArrayList<String>() // non-null (constructor result) list.add("Item")
val size = list.size // non-null (primitive int) val item = list[0] // platform
type inferred (ordinary Java object) item.substring(1) // allowed, may throw an
                                                       // exception if item == null

將平台值指派給 Kotlin 變數時,Kotlin 需要進行類型推論,或者您也可以定義預期類型。如要確保來自 Java 的參照處於正確的是否可為空值狀態,最好的做法是在 Java 程式碼中使用是否可為空值註解 (例如 @Nullable)。Kotlin 編譯器會將這些參照視為實際的可為空值或不可為空值的類型,而非平台類型。

Java Jetpack API 會視需要使用 @Nullable@NonNull 進行註解,而 Android 11 SDK 中也採取了類似的做法。在 Kotlin 中採用的這個 SDK 的類型,會表示為正確的可為空值或不可為空值的類型。

由於 Kotlin 的類型系統,應用程式遇到 NullPointerException 的當機次數有明顯降低的趨勢。舉例來說,在將新功能開發遷移至 Kotlin 的一年中,Google Home 應用程式因空值指標例外狀況導致的當機次數降低了 30%。