Tina Tang's Blog

在哪裡跌倒了,就在哪裡躺下來

0%

Android筆記(7)-偵錯(Debugging)

只要是軟體使用者,很可能都曾遇到錯誤。「bug」是指某個軟體導致錯誤的行為,例如應用程式當機或功能無法正常運作。無論經驗為何,所有開發人員在撰寫程式碼時都會回報錯誤,而 Android 開發人員最重要的技能之一就是辨識及修正。

bug的修正程序稱為偵錯(debugging)。知名電腦科學家 Brian Kernighan 曾表示,「最有效的偵錯工具至今仍在審慎考量,加上眾所皆知的印刷品聲明。」 您可能已經很熟悉先前程式碼研究室的 Kotlin’s println() 陳述式,但專業的 Android 開發人員會使用記錄功能來更妥善地整理程式的輸出內容。在這個程式碼研究室中,您將瞭解如何使用 Android Studio 中的記錄功能,以及如何將記錄用於偵錯工具。您將瞭解如何讀取錯誤訊息記錄 (稱為堆疊追蹤),藉此找出並修正錯誤。

學習目標

  • 使用 android.util.Logger 寫入記錄檔。
  • 瞭解不同記錄檔層級的使用時機。
  • 使用記錄是一項簡單的強大工具。
  • 如何在堆疊追蹤中尋找有意義的資訊。
  • 搜尋錯誤訊息以解決應用程式當機問題。

建立新專案

我們將從空白專案開始,先顯示一個空白專案來示範記錄陳述式及其偵錯用途,而不是使用大型的應用程式。
請先建立新的 Android Studio 專案,如圖所示。

  1. 在「New Project」(新增專案) 畫面中,選擇「Empty Views Activity」(空白活動)。

  2. 將應用程式命名為「Debugging」。確認語言已設為 Kotlin,且其他維持不變。


對輸出進行記錄與偵錯

在先前的課程中,您曾使用 Kotlin 的 println() 陳述式產生文字輸出。在 Android 應用程式中,記錄記錄的最佳做法是使用 Log 類別。記錄輸出功能有多種函式,採 Log.v()Log.d()Log.i()Log.w()Log.e() 格式。這些方法有兩種參數,第一個稱為「標記」,是識別記錄訊息來源 (例如記錄文字的類別名稱) 的字串。第二個則是實際記錄訊息

執行下列步驟,開始在空白專案中使用記錄功能。

  1. MainActivity.kt 的類別宣告之前,新增名為 TAG 的常數,並將該值設為類別的名稱 MainActivity
1
private const val TAG = "MainActivity"

注意: 記錄標記通常是在類別外宣告。雖然此變數是在 MainActivity 之外宣告,但會宣告為私人,因此只能在 MainActivity.kt 中存取。也就是說,您也可以宣告其他類別的 TAG 變數。

  1. 向 MainActivity 類別新增一個名為 logging() 的新函數,如下所示。
1
2
3
fun logging() {
Log.v(TAG, "Hello, world!")
}
  1. onCreate() 中呼叫 logging()
1
2
3
4
5
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
logging()
}
  1. 執行應用程式以查看記錄。記錄檔隨即顯示在畫面底部的 Logcat 視窗中。在篩選框中輸入 level:debug ,藉此排除與您應用程式無關的任何記錄。

  2. 您應該會在輸出視窗中看到「Hello, world!」。如有需要,請在 Logcat 視窗頂端的搜尋框中輸入「hello」,即可搜尋所有記錄。


記錄層級

造成不同記錄檔函式 (名稱不同) 的原因,原因是這些相對應的記錄層級不同。您可以根據要輸出的資訊類型,在 Logcat 輸出內容中使用不同記錄檔層級進行篩選。您會定期使用五個主要記錄層級。

紀錄層級 用途 範例
錯誤(ERROR) 錯誤記錄會回報發生嚴重錯誤,例如應用程式當機的原因。 Log.e(TAG, “The cake was left in the oven for too long and burned.”).
警告(WARN) WARN 記錄較不嚴重,但仍會回報應修正的問題,以避免嚴重的錯誤。舉例來說,如果您呼叫的函式「已淘汰」,我們不建議使用該函式取代較新的函式。 Log.w(TAG, “This oven does not heat evenly. You may want to turn the cake around halfway through to promote even browning.”)
資訊(INFO) INFO 記錄提供實用資訊,例如成功完成的作業。 Log.i(TAG, “The cake is ready to be served.”).println(“The cake has cooled.”)
偵錯(DEBUG) DEBUG 記錄包含調查問題時可能會用到的資訊。這些版本不會顯示在發布版本中,例如您在 Google Play 商店發布的版本。 Log.d(TAG, “Cake was removed from the oven after 55 minutes. Recipe calls for the cake to be removed after 50 - 60 minutes.”)
詳細程度(VERBOSE) 顧名思義,「VERBOSE」是最低的記錄層級。何謂偵錯記錄與詳細記錄其實有點主觀,但一般而言,詳細記錄可以在功能實作之後移除,而偵錯記錄在偵錯時可能仍然有用。這些版本不包含版本。 Log.v(TAG, “Put the mixing bowl on the counter.”)Log.v(TAG, “Grabbed the eggs from the refrigerator.”)Log.v(TAG, “Plugged in the stand mixer.”)

請注意,目前並沒有設定各類型記錄層級的使用規則,特別是使用 DEBUGVERBOSE 的時機。軟體開發團隊可能會制定個別記錄層級的使用時機,或是決定不採用特定記錄層級 (例如 VERBOSE)。這兩個記錄層級有一重要的重點是,這些版本並沒有發布版本,因此使用記錄偵錯功能不會影響已發布應用程式的效能,而 println() 陳述式則保留在發布版本中,並對負面影響產生負面影響成效。

讓我們看看 Logcat 中各種不同的記錄層級。

  1. MainActivity.kt 中,將 logging() 方法的內容替換為下列內容。
1
2
3
4
5
6
7
fun logging() {
Log.e(TAG, "ERROR: a serious error like an app crash")
Log.w(TAG, "WARN: warns about the potential for serious errors")
Log.i(TAG, "INFO: reporting technical information, such as an operation succeeding")
Log.d(TAG, "DEBUG: reporting technical information useful for debugging")
Log.v(TAG, "VERBOSE: more verbose than DEBUG logs")
}
  1. 執行您的應用程式,並在 Logcat 中觀察輸出內容。輸入 tag: MainActivity,篩選出只顯示含有「MainActivity」代碼的記錄。

使用鍵/值搜尋查詢記錄

在 Android Studio 中,您可以直接透過主查詢欄位產生鍵/值搜尋。這個查詢系統可提供精準的查詢結果,也能根據鍵/值排除記錄。儘管您可以選擇使用規則運算式,但在查詢上並非必要。如要查看建議項目,請在查詢欄位中按下 Ctrl + Space 鍵。

以下提供幾個可用於查詢的鍵範例:

  • tag:比對記錄項目的 tag 欄位。
  • package:比對記錄應用程式的套件名稱。
  • process:比對記錄應用程式的程序名稱。
  • message:比對記錄項目的訊息部分。
  • level:比對指定或更嚴重的記錄層級,例如 DEBUG。
  • age:比對項目時間戳記是否最新。值的指定方式是以數字後面加上代表時間單位的字母:s 代表秒數、m 代表分鐘數、h 代表小時數,d 則代表天數。例如,age: 5m 只會篩選出過去 5 分鐘內記錄的訊息。

更詳細的介紹請點這裡查看。


含有錯誤訊息的記錄

在空白專案中無法偵錯。Android 開發人員經常遇到許多錯誤,那就是應用程式當機,但用戶體驗不佳。讓我們新增一些程式碼,讓這個應用程式當機。

  1. 將下列函式新增至 logging() 函式的 MainActivity.kt。這個程式碼以兩個數字開頭,並使用 repeat 來將分子除以五分子的結果。每次執行 repeat 區塊中的程式碼時,分母的值就會減少 1。在第 5 次和最後一次疊代時,應用程式嘗試除以 0。
1
2
3
4
5
6
7
8
fun division() {
val numerator = 60
var denominator = 4
repeat(5) {
Log.v(TAG, "${numerator / denominator}")
denominator--
}
}
  1. 新增division() 函式到 onCreate() 中的 logging() 後。
1
2
3
4
5
6
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
logging()
division()
}
  1. 再次執行應用程式,向下捲動至 MainActivity.kt 類別中的記錄,即可查看先前定義的 logging() 函式中的日誌、「division()」函式的詳細記錄,以及說明應用程式為什麼當機的紅色錯誤記錄。

堆疊追蹤剖析

說明當機的錯誤記錄 (也稱為例外狀況) 亦稱作堆疊追蹤。堆疊追蹤會顯示所有已觸發至例外狀況的函式,而且系統會從最近呼叫的時間開始呼叫。完整輸出內容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Process: com.example.debugging, PID: 14581
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.debugging.MainActivity.division(MainActivity.kt:21)
at com.example.debugging.MainActivity.onCreate(MainActivity.kt:14)
at android.app.Activity.performCreate(Activity.java:8000)
at android.app.Activity.performCreate(Activity.java:7984)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
如何查看錯誤
  1. java.lang.RuntimeException:
1
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero

第一行代表應用程式無法開始活動,這就是應用程式當機的原因。下一行會提供更多資訊。具體來說,該活動無法啟動的原因是 ArithmeticException。具體來說,ArithmeticException 的類型是「除以 0」。

  1. Caused by:
1
2
Caused by: java.lang.ArithmeticException: divide by zero
at com.example.debugging.MainActivity.division(MainActivity.kt:21)

如果向下捲動頁面顯示「Caused by:」一行,表示發生「除以 0」的錯誤。另會顯示錯誤發生的確切函式 (division()) 和確切的行數 (21)。Logcat 視窗中的檔案名稱和行數是超連結。輸出結果也會列出發生錯誤的函式名稱 (division()) 和呼叫該函式的函式 (onCreate())。


使用記錄檔找出並修正錯誤

使用記錄陳述式,透過輸出分母值來避免除數為零的情況,從而節省時間。

  1. Log.v() 之前,新增 Log.d() 呼叫記錄分母。Log.d() 的用途是偵錯,因此可用來篩選詳細記錄。
1
Log.d(TAG, "$denominator")
  1. 再次執行應用程式。雖然分母仍持續當機,但分母應該記錄多次。您可以使用篩選條件設定,只顯示含有 “MainActivity” 標記的記錄。
  1. 您可以看到有多個值輸出。當分母為 0 時,迴圈會在第五次疊代之前執行數次。如要修正錯誤,您可以將迴圈中的疊代次數從 5 變更為 4。再次執行應用程式時,應用程式不會再停止運作。
1
2
3
4
5
6
7
8
9
fun division() {
val numerator = 60
var denominator = 4
repeat(4) {
Log.d(TAG, "$denominator")
Log.v(TAG, "${numerator / denominator}")
denominator--
}
}