1. 准备工作
现在,大多数新手开发者可能都知道使用日志语句进行调试。学完第 1 单元后,您还将了解如何阅读堆栈轨迹和研究错误消息。虽然二者都是强大的调试工具,但现代 IDE 能够提供更多功能,使调试过程更加高效。
在这节课中,您将了解 Android Studio 的集成式调试程序、如何暂停执行应用,以及如何一次执行一行代码,以找到 bug 的确切来源。此外,您还将了解如何使用名为“Watches”的功能,并跟踪特定变量,而无需添加特定的日志语句。
前提条件
- 您知道如何在 Android Studio 中浏览项目。
- 熟悉 Kotlin 日志记录功能。
学习内容
- 如何将调试程序连接到正在运行的应用。
- 使用断点暂停正在运行的应用,并且一次检查一行代码。
- 向断点添加条件表达式,以节省调试时间。
- 向“Watches”窗格添加变量,以帮助进行调试。
所需条件
- 一台安装了 Android Studio 的计算机。
2. 创建一个新项目
我们不是调试一个复杂的大型应用,而是从一个空白项目着手,并引入一些有 bug 的代码,以演示 Android Studio 中的调试工具。
首先,创建一个新的 Android Studio 项目。
- 在 Select a Project Template 屏幕上,选择 Blank Activity。
- 将应用命名为 Debugging,确保将语言设置为 Kotlin,其余所有内容都保持不变。
- 您将看到一个新的 Android Studio 项目,其中显示了一个名为
MainActivity.kt
的文件。
引入 bug
还记得第 1 单元的调试课中的除数为零示例吗?在循环的最后迭代中,当应用尝试除以零时,应用会崩溃并抛出 java.langArithmeticException
,因为除数不能为零。我们通过检查堆栈轨迹找到并修复了该 bug,并使用日志语句验证了这一假设。
因为您已熟悉该示例,所以我们将使用它来演示如何使用断点。断点可逐步一次运行一行代码,无需先添加日志语句并重新运行应用。
- 打开
MainActivity.kt
并将代码替换为以下代码:
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
public val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
division()
}
fun division() {
val numerator = 60
var denominator = 4
repeat(5) {
Log.v(TAG, "${numerator / denominator}")
denominator--
}
}
}
- 执行应用。注意应用是否按预期崩溃。
3. 使用断点进行调试
在学习日志记录时,您学习了有策略地放置日志,以帮助发现 bug 并验证 bug 是否已修复。不过,如果遇到并非由您引入的 bug,有时候就无法确定应该在何处放入日志语句或输出哪些变量。通常,您只能在运行时找到这些信息。
fun division() {
val numerator = 60
var denominator = 4
repeat(5) {
Log.v(TAG, "${numerator / denominator}")
denominator--
}
}
这时候断点就派上用场了!即使您根据堆栈轨迹中的信息大致知道是什么导致了 bug,也可以添加断点,以此作为特定代码行的停止符号。到达某个断点后,代码会暂停执行,使您能够在运行时使用其他调试工具,以便仔细查看发生的情况和问题所在。
连接调试程序
在后台,Android Studio 会使用一个名为 Android 调试桥(简称 ADB)的工具。这是一个集成到 Android Studio 中的命令行工具,可以为正在运行的应用提供调试功能,如断点。用于调试的工具通常称为“调试程序”。
为了将调试程序用于(连接到)应用,您不能像以前那样简单地通过依次点击 Run > Run 来运行应用,而应通过依次点击 Run > Debug ‘app' 来运行应用。
向项目中添加断点
执行以下步骤来看看断点的实际运用:
- 点击您希望在其位置暂停的行号旁边的空白处来添加一个断点。该行号旁边会出现一个点,并且该行会突出显示。
- 依次点击 Run > Debug ‘app' 或使用工具栏中的 图标,在连接了调试程序的情况下运行应用。应用启动时,您应该会看到如下所示的屏幕:
应用启动后,您会看到断点被激活时突出显示。
在您之前查看 Logcat 窗口的屏幕底部,系统打开了一个新的 Debug 标签页。
左侧是函数列表,它们与堆栈轨迹中显示的列表相同。右侧是一个窗格,您可以在其中查看当前函数(即 division()
)中各个变量的值。顶部还提供了按钮,可以帮助您在已暂停的程序中导航。您将最常用的一个按钮是 Step Over,点按后会执行突出显示的一行代码。
执行以下步骤来调试代码:
- 到达断点后,第 19 行(声明
numerator
变量)现已突出显示,但尚未运行。使用 Step Over 按钮执行第 19 行。现在,第 20 行将突出显示。
- 在第 22 行设置一个断点。这是出现除法的位置,也是堆栈轨迹报告异常的那一行。
- 使用 Debug 窗口左侧的 Resume Program 按钮转到下一个断点,并运行
division()
函数的其余部分。
- 请注意,在第 17 行停止了执行,未执行该行。
- 每个变量(
numerator
和denominator
)的值都显示在其声明旁边。您可以在“Debug”窗口的 Variables 标签页中看到这些变量的值。
- 再按“Debug”窗口左侧的 Resume Program 按钮 4 次。每次循环暂停并观察
numerator
和denominator
的值。在最后一次迭代时,numerator
应该为60
,denominator
应该为0
。但不能将 60 除以 0!
现在,您已经知道了导致 bug 的确切代码行,并且知道了确切的原因。和之前一样,您可以通过将重复代码的次数从 5
更改为 4
来修复 bug。
fun division() {
val numerator = 60
var denominator = 4
repeat(4) {
Log.v(TAG, "${numerator / denominator}")
denominator--
}
}
4. 设置断点的条件
在上一部分中,您必须遍历每个循环迭代,直到分母为零。在更复杂的应用中,如果您掌握的 bug 信息较少,这可能会很繁琐。不过,如果您有一个假设(例如,应用只有在分母为零时才会崩溃),则可以修改断点,以便仅在满足此假设时才到达断点,而不必遍历每个循环迭代。
- 如有必要,通过在重复循环中将
4
更改为5
,重新引入 bug。
repeat(4) {
...
}
- 在包含
repeat
语句的行上添加新的断点。
- 右键点击红色断点图标。这时会出现一个菜单,其中包含一些选项,例如是否启用断点。已停用的断点仍然存在,但不会在运行时被触发。您还可以选择添加 Kotlin 表达式:如果计算结果为 true,则会触发断点。例如,如果您使用
denominator > 3
表达式,则该断点将仅在循环第一次迭代时触发。如需仅在应用有可能要除以零时触发该断点,请将表达式设置为denominator == 0
。断点的选项应如下所示:
- 依次点击 Run > Debug 'app' 运行应用,并观察是否已到达断点。
您可以看到分母已经为 0。系统只有在满足条件时才会触发断点,这样就无需遍历代码,节省了时间和精力。
- 和之前一样,您会看到 bug 是由循环执行一行代码次数过多导致的,此时分母设置为 0。
添加监测条件
如果您想在调试时监控特定值,则无需在 Variables 标签页中搜索查找该值。您可以添加“Watches”来监控特定变量。这些变量将显示在调试窗格中。当执行暂停且变量在作用域内时,它将在“Watches”窗格中显示。这样可以提高处理大型项目时的调试效率。您将能够在一个位置跟踪所有相关变量。
- 在调试视图中,在“Variables”窗格的右侧应该还有一个名为 Watches 的空白窗格。点击左上角的加号 按钮。您会看到一个菜单选项,显示 New Watch。
- 在提供的字段中输入变量名称
denominator
,然后点击 Enter。 - 依次点击 Run > debug'app' 重新运行应用,您会发现:在遇到断点时,您会在“Watches”窗格中看到分母的值。
5. 恭喜
总结:
- 您可以设置断点来暂停应用的执行。
- 执行暂停后,您可以“单步”只执行一行代码。
- 您可以将条件语句设置为仅根据 Kotlin 表达式触发断点。
- 通过“Watches”功能,您可以在调试时对感兴趣的变量进行分组。