偵錯簡介

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

1. 事前準備

使用軟體的人很可能遇到錯誤。「錯誤」是指某個軟體導致錯誤的行為,例如應用程式當機或功能無法正常運作。無論經驗為何,所有開發人員在撰寫程式碼時都會回報錯誤,而 Android 開發人員最重要的技能之一就是辨識及修正。我們發現經常發布錯誤的應用程式版本並不常見。請見下方 Google 地圖的版本詳細資料:

9d5ec1958683e173.png

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

必要條件

  • 您已瞭解如何在 Android Studio 中瀏覽專案。

課程內容

本課程結束時,您將能夠

  • 使用 android.util.Logger 寫入記錄檔。
  • 瞭解不同記錄檔層級的使用時機。
  • 使用記錄是一項簡單的強大工具。
  • 如何在堆疊追蹤中尋找有意義的資訊。
  • 搜尋錯誤訊息以解決應用程式當機問題。
  • 從 Android 模擬器擷取螢幕截圖和 GIF 動畫。

軟硬體需求

  • 已安裝 Android Studio 的電腦。

2. 建立新專案

往後,我們將從空白專案開始,先顯示一個空白專案來示範記錄陳述式及其偵錯用途,而不是使用大型的應用程式。

請先建立新的 Android Studio 專案,如圖所示。

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

72a0bbf2012bcb7d.png

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

60a1619c07fae8f5.png

建立專案後,您會收到一個新的 Android Studio 專案,其中會顯示一個名為 MainActivity.kt 的檔案。

e3ab4a557c50b9b0.png

3. 記錄和偵錯輸出

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

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

  1. MainActivity.kt 的類別宣告之前,新增名為 TAG 的常數,並將該值設為類別的名稱 MainActivity
private const val TAG = "MainActivity"
  1. 將新的函式新增至名為 logging()MainActivity 類別,如圖所示。
fun logging() {
    Log.v(TAG, "Hello, world!")
}
  1. 致電位於onCreate()logging()。新的 onCreate() 方法如下所示。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. 執行應用程式以查看記錄。記錄檔隨即顯示在畫面底部的 Logcat 視窗中。由於 Logcat 會在裝置 (或模擬器) 上顯示其他程序的輸出內容,您可以從下拉式選單中選取您的應用程式 (com.example.debugging),藉此排除與您應用程式無關的任何記錄。

199c65d11ee52b5c.png

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

92f258013bc15d12.png

記錄層級

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

紀錄層級

用途

範例

錯誤

錯誤記錄會回報發生嚴重錯誤,例如應用程式當機的原因。

Log.e(TAG, "The cake was left in the oven for too long and burned.").

警告

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 記錄提供實用資訊,例如成功完成的作業。

Log.i(TAG, "The cake is ready to be served.").println("The cake has cooled.")

偵錯

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.")

詳細程度

顧名思義,「詳細」是最低的記錄層級。何謂偵錯記錄 (而非詳細記錄) 有一種主觀感,但一般而言,詳細記錄是在功能實作之後移除的,而偵錯記錄在偵錯時可能很有用。這些版本不包含版本。

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() 方法的內容替換為下列內容。
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 中觀察輸出內容。如有需要,請篩選輸出結果,只顯示 com.example.debugging 程序的記錄。您也可以篩選輸出內容,只顯示含有「MainActivity」代碼的記錄。方法是從 Logcat 視窗右上角的下拉式選單中,選取「Edit Filter Configuration」(編輯篩選條件設定)

383ec6d746bb72b1.png

  1. 然後在「Log Tag」(記錄標記) 中輸入「MainActivity」,並為篩選條件建立名稱。

e7ccfbb26795b3fc.png

  1. 現在應該只會看到含有「MainActivity」標記的記錄訊息。

4061ca006b1d278c.png

請注意,類別名稱之前已有英文字母,例如 W/MainActivity,對應至記錄層級。此外,WARN 記錄會以藍色顯示,ERROR 記錄則以紅色顯示,就像上一個示例中的嚴重錯誤一樣。

  1. 您可以按照程序篩選偵錯輸出內容,也可以按照記錄層級篩選輸出內容。預設設定是「Verbose」(詳細),顯示 VERBOSE 記錄和更高層級的記錄層級。在下拉式選單中選取「Warn」(警告),但請注意,系統只會顯示 WARNERROR 層級的記錄。

c4aa479a8dd9d4ca.png

  1. 再次將下拉式選單變更為「Assert」,並觀察未顯示任何記錄。這麼做會過濾掉 ERROR 級以下的所有結果。

ee3be7cfaa0d8bd1.png

雖然這樣處理 println() 陳述式看起來有點小題大作,但是當建構更大型的應用程式時,Logcat 也會有更多輸出內容,這種情況下記錄層級就能幫助您找出最實際、最相關的資訊。使用記錄是最佳做法,而且在 Android 開發作業中優先於 println(),因為偵錯和詳細記錄不會影響發布版本的效能。也可以根據不同的記錄層級篩選記錄。選擇正確的記錄層級不僅能讓開發團隊中不像您一樣熟悉程式碼的其他人收益,也能讓您更輕易找出並解決錯誤。

4. 含有錯誤訊息的記錄檔

回報錯誤

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

請記住,您在數學課程中學到的技巧,無法將數字除以零。以下說明當程式碼嘗試除以 0 時,會發生什麼情況。

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

12d87f287661a66.png

堆疊追蹤剖析

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

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:
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:
Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)

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

這些錯誤應該不是出乎意料,因為錯誤已刻意導入。但如果您需要判斷不明錯誤的原因,只要知道例外狀況的類型、函式名稱和行號,就能提供極為實用的資訊。

為什麼要使用「堆疊追蹤」?

「堆疊追蹤」一詞看起來就像是很奇常的字詞,用來描述錯誤中的文字輸出內容。如要進一步瞭解這項功能的運作方式,您需要進一步瞭解函式堆疊。

當一個函式呼叫另一個函式時,在第二個函式完成之前,裝置不會執行第一個函式的任何程式碼。當第二個函式執行完畢後,第一個函式就會從上次中斷的地方接續執行。第二個函式呼叫的任何函式也同樣適用。第二個函式必須等到第三個函式完成 (並會呼叫其呼叫的任何其他函式) 後,才會繼續執行;在第二個函式完成執行後,第一個函式也不會恢復。這與實體世界中的堆疊類似,例如一疊盤子或一堆卡片。如果您想拿一個盤子,您會首先拿最上面的盤子。沒有先移除上面所有的固定板,就無法在堆疊中移走。

函式程式碼可透過以下程式碼來說明。

val TAG = ...

fun first() {
    second()
    Log.v(TAG, "1")
}

fun second() {
    third()
    Log.v(TAG, "2")
    fourth()
}

fun third() {
    Log.v(TAG, "3")
}

fun fourth() {
    Log.v(TAG, "4")
}

如果撥打 first(),系統就會按以下順序記錄號碼。

3
2
4
1

原因是什麼呢?呼叫第一個函式後,會立即呼叫 second(),因此系統無法立即記錄號碼 1。函式堆疊如下所示。

second()
first()

第二個函式會呼叫 third(),將其新增至函式堆疊。

third()
second()
first()

第三個函式接著會列印 3 編號。執行作業完成後,就會從函式堆疊中移除。

second()
first()

second() 函式會記下號碼 2,然後呼叫 fourth()。目前為止,系統會記錄 32 的數值,且函式堆疊如下。

fourth()
second()
first()

fourth() 函式會列印數字 4,並將其從函式堆疊中移除 (彈出)。接著,second() 函式會執行完畢,並彈出函式堆疊。現在 second() 和其呼叫的所有函式都已完成,裝置接著會在 first() 中執行剩餘程式碼,以便列印數字 1

因此,號碼會按以下順序記錄:4231

只要花一點時間瞭解程式碼並逐步說明內容堆疊的用心圖片,就能查看實際執行的程式碼和執行順序。單方面可以有效解決偵錯錯誤,例如除以零的例子就發生錯誤。您也可以藉由檢查程式碼,判斷在哪個位置放置記錄陳述式,協助排除更複雜的問題。

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

在上一節中,您查看了堆疊追蹤,特別是這行。

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

在這個步驟中,您可以得知當機事件是在第 21 行開始,且與零相關。因此,在這段程式碼之前執行的分母為 0。雖然您可以試著逐步剖析程式碼,這樣在這個小例子中也能順利執行程式碼,但您也可以使用記錄陳述式,透過輸出分母值來避免除數為零的情況,從而節省時間。

  1. Log.v() 之前,新增 Log.d() 呼叫記錄分母。Log.d() 的用途是偵錯,因此可用來篩選詳細記錄。
Log.d(TAG, "$denominator")
  1. 再次執行應用程式。雖然分母仍持續當機,但分母應該記錄多次。您可以使用篩選條件設定只顯示含有 "MainActivity" 標記的記錄。

d6ae5224469d3fd4.png

  1. 您可以看到多個列印值。當分母為 0 時,迴圈會在第五次疊代之前執行數次。這種做法很合理,因為分母為 4,而迴圈會在 1 中減少 5 疊代。如要修正錯誤,您可以將迴圈中的疊代次數從 5 變更為 4。再次執行應用程式時,應用程式不會再停止運作。
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

6. 偵錯範例:存取不存在的值

根據預設,您用於建立專案的空白活動範本會新增一項活動,並將 TextView 置於畫面中央。如同先前所述,您可以在「版面配置編輯器」中設定 ID,以及使用 findViewById() 存取資料檢視畫面,藉此參照程式碼的資料檢視畫面。在活動類別中呼叫 onCreate() 時,必須先呼叫 setContentView() 載入版面配置檔案 (例如 activity_main.xml)。如果您在呼叫 setContentView() 之前呼叫了 findViewById(),應用程式就會因為當機而不存在。讓我們試著存取檢視畫面,以解釋其他錯誤。

  1. 打開 activity_main.xml,請選取「Hello world!」TextView,並將 id 設為 hello_world

c94be640d0e03e1d.png

  1. 返回 onCreate() 中的 ActivityMain.kt,並新增程式碼取得 TextView,然後在呼叫 setContentView() 前將其文字變更為 「Hello, debugging!」。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val helloTextView: TextView = findViewById(R.id.hello_world)
    helloTextView.text = "Hello, debugging!"
    setContentView(R.layout.activity_main)
    division()
}
  1. 再次執行應用程式,但請注意,應用程式啟動時才會立即當機。您可能需要移除前一個範例中的篩選條件,才能查看不含 "MainActivity" 標記的記錄。840ddd002e92ee46.png

例外狀況應該是 Logcat 中顯示的最後一個項目 (如果沒有的話,請搜尋 RuntimeException)。輸出內容看起來會像如下所示。

Process: com.example.debugging, PID: 14896
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null
        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.NullPointerException: findViewById(R.id.hello_world) must not be null
        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)

就像之前一樣,畫面頂端會顯示「無法開始活動」。這個情況相當合理,因為應用程式在 MainActivity 推出前就已經當機。下一行會進一步說明這項錯誤。

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

在堆疊追蹤中,您也會看見這行程式碼,並列出實際的函式呼叫和行號。

Caused by: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

這個錯誤代表什麼意義,到底「空值」是什麼?雖然這是個相當牽強的例子,而且您可能已經對應用程式當機的原因有所瞭解,但您難免會遇到之前沒見過的錯誤訊息。遇到這種情況時,雖然您不是第一位看到錯誤,但最經驗豐富的開發人員常會看到 Google 的錯誤訊息,藉此瞭解其他人如何解決問題。查出這項錯誤會產生來自 StackOverflow 網站的多個結果。開發人員可以在該網站提問,也可以針對錯誤程式碼或較一般的程式設計主題提供解答。

有許多問題相似且不完全的答案,因此在搜尋解答時,請記住下列提示。

  1. 回覆的時間有多久?多年來的回覆很可能已無關聯,或使用了過時的語言或架構。
  2. 答案是使用 Java 還是 Kotlin?您的問題是否與某種特定語言有關,或與特定架構有關?
  3. 標示為「已接受」或認同程度較高的答案可能具有較高的品質,但請注意,其他答案可能也會有實用的資訊。

1636a21ff125a74c.png

數字代表認同票數 (或反對票數) 的數字,綠色勾號表示已接受的答案。

如果找不到現有的問題,可以隨時提問。在 StackOverflow (或任何網站) 上提問時,請記住 這些規範

請直接搜尋錯誤

a60ba40e5247455e.png

在看過幾種答案之後,您可以發現錯誤可能原因有很多種。不過,由於您是在 setContentView()前刻意使用 findViewById(),因此針對 這個問題 提供了一些解答。第二票投票數表示:

「這可能是因為你在呼叫 setContentView 之前是呼叫 FindViewById?如果是這種情況,請嘗試在呼叫 setContentView 呼叫 findViewById」

看到這個答案後,您就可以在程式碼內驗證,對 findViewById() 的呼叫在 setContentView() 之前確實過早,應改為在 setContentView() 之後呼叫。

更新程式碼即可修正錯誤。

  1. 將通話轉接至 findViewById(),以及在通話下方設定 helloTextView 文字的行就是 setContentView()。新的 onCreate() 方法如下所示。您也可以視需要新增記錄,驗證錯誤是否已修正。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.d(TAG, "this is where the app crashed before")
    val helloTextView: TextView = findViewById(R.id.hello_world)
    Log.d(TAG, "this should be logged if the bug is fixed")
    helloTextView.text = "Hello, debugging!"
    logging()
    division()
}
  1. 重新執行應用程式。請注意,應用程式已不再當機,且文字會如預期更新。

9ff26c7deaa4a7cc.png

擷取螢幕截圖

您目前看過許多 Android 模擬器的螢幕截圖。擷取螢幕畫面相對簡單,但也可用來分享資訊,例如向其他團隊成員重現錯誤的步驟。只要按下右側工具列中的相機圖示,即可在 Android 模擬器中擷取螢幕畫面。

455336f50c5c3c7f.png

您也可以使用鍵盤快速鍵 Command+S 拍攝螢幕截圖。螢幕截圖會自動儲存到「桌面」資料夾。

錄製執行中的應用程式

雖然螢幕截圖可以傳達許多資訊,但有時也可以分享正在執行的應用程式錄製內容,協助他人重現造成錯誤的內容。Android 模擬器提供部分內建工具,可讓您輕鬆擷取運作中應用程式的 GIF 動畫。

  1. 在右側的 Emulator 工具中,按一下「More」(更多內容) 按鈕 558dbea4f70514a8.png (最後一個選項),即可顯示其他模擬器偵錯選項。畫面上彈出的視窗提供額外的工具,用於模擬實體裝置的功能以供測試。

46b1743301a2d12.png

  1. 在左選單中按一下「Record and Playback」,畫面上會顯示一個按鈕,按下按鈕即可開始錄製。

dd8b5019702ead03.png

  1. 目前,除了靜態 TextView 以外,您的專案還沒有任何可錄製的內容。讓我們修改程式碼,每隔幾秒就更新標籤以顯示部門結果。在 MainActivitydivision() 方法中,先呼叫 Thread.sleep(3000) 再呼叫 Log()。這個方法現在應該如下所示 (請注意,迴圈應僅重複 4 次,以免當機)。
fun division() {
   val numerator = 60
   var denominator = 4
   repeat(4) {
       Thread.sleep(3000)
       Log.v(TAG, "${numerator / denominator}")
       denominator--
   }
}
  1. activity_main.xml 中,將 TextViewid 設為 division_textview

db3c1ef675872faf.png

  1. 返回 MainActivity.kt,請將 Log.v() 呼叫替換為下列 findViewById()setText() 呼叫,將文字設為商數。
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
  1. 由於現在是在應用程式 UI 中顯示除法的結果,因此您必須留意 UI 更新作業的一些細節。首先,您必須建立可執行 repeat 迴圈的新執行緒。否則,Thread.sleep(3000) 會封鎖主執行緒,而且在 onCreate() 完成 (包括具有 repeat 迴圈的 division()) 之前,應用程式檢視畫面不會顯示。
fun division() {
   val numerator = 60
   var denominator = 4

   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
         denominator--
      }
   }
}
  1. 如果現在嘗試執行應用程式,系統會顯示 FATAL EXCEPTION。出現此例外狀況的原因是只有建立檢視畫面的執行緒能夠進行變更。幸好,您可使用 runOnUiThread() 參照 UI 執行緒。變更 division(),以更新 UI 執行緒中的 TextView
private fun division() {
   val numerator = 60
   var denominator = 4
   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         runOnUiThread {
            findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
            denominator--
         }
      }
   }
}
  1. 執行應用程式,然後立即切換至模擬器。應用程式啟動後,按一下「Extended Controls」視窗中的「Start Recording」按鈕。商數應該每 3 秒更新一次。更新幾次商數之後,按一下「Stop Recording」(停止錄製)

55121bab5b5afaa6.png

  1. 根據預設,輸出結果會以 .webm 格式儲存。使用下拉式選單將輸出內容匯出為 GIF 檔案。

850713aa27145908.png

7. 恭喜

恭喜!在這個課程中,您學到:

  • 偵錯是疑難排解程式碼錯誤的程序。
  • 記錄檔可讓您以不同的記錄層級和標記列印文字。
  • 堆疊追蹤可提供例外狀況的相關資訊,例如引發該問題的確切函式,以及發生例外狀況的行數。
  • 排除錯誤時,用戶很可能遇到相同或類似的問題,而您可以使用 StackOverflow 這類網站來研究錯誤。
  • 使用 Android 模擬器即可輕鬆匯出螢幕截圖和動畫 GIF。

瞭解詳情