作为开发者,我们有时会被一些问题所困,导致在调试器中所花费的时间甚至超过了编写代码所用的时间。正因如此,最近我们找机会了解了 Android Studio 团队在提升调试速度方面使用的一些技巧。接下来,我们会为您一一呈现那些我们认为最好的、节省您时间的、且方便与您的调试流程整合的小技巧。
虽然您的应用可能与本文假想中的示例应用大相径庭,但是本文所介绍的小窍门可以用在任何应用的开发上。
Log 的过滤与折叠
我们从经典调试法 —— printf 语句的一个小窍门说起。假设有一个游戏,它会在日志中打印它的帧数和用户的最终得分,那么该游戏就会在 Logcat 窗口中给出以下内容:
这里的输出信息中可能包含了许多您并不关心的内容,例如日期和线程 ID。您其实可以轻松地调整内容的显示,只需要点击 Logcat 工具栏中的设置图标,便会弹出一个 Configure Logcat Header 窗口,您可以在这里取消那些您不想看到的信息:
这样一来,您便获得了更加简洁、更加相关的日志输出,如下:
然而,这里仍然留下了许多混淆视听的内容,妨碍我们专注于 High Score 信息。您可以使用搜索功能解决这一问题,只要在 搜索 中输入一部分调试信息来对 Logcat 窗口进行过滤:
您经常会用到的搜索条件,可以通过 Edit Filter Configuration 添加到自定义过滤器中:
下面,添加过滤器的详细信息:
另一种减少混乱日志的方式是使用折叠功能,它可以把近似的日志折叠为同一组。您只需选中一条日志中的部分文本,右击鼠标,并选择 Fold Lines Like This :
当 Console 对话框出现时,点击 OK ,就可以将包含选中文本的近似日志整理到一起:
如果稍后您需要查看被折叠的信息,则可以点击某行来展开内容。这里也提供了方便您展开或收起折叠行的按钮。
在当前进程附加调试器
虽然我们可以通过 Debug 按钮或菜单选项启动一个调试会话,但当您想要调试一个已经启动的应用时,也可以为其附加一个调试器,这样就可以不用重启应用。您可以点击 Attach Debugger to Android Process 按钮来执行这一操作:
在 Choose Process 弹窗中,选中您希望附加调试器的进程并且点击 OK 。接下来,和普通的调试会话中一样,调试器会开始触发您的断点。
移动断点
如果您发现断点添加的位置不合适,除了清除并重设断点外,您还可以将当前的断点拖动至您想要的地方。这个功能十分有用,因为移动操作保留了该断点的设置,其中包括了许多本文接下来将会介绍的特性。
条件断点
您可能需要找到应用或游戏中与特定类型事件有关的 Bug。举例来说,在一个正在开发的游戏中,您可能希望在玩家的角色与物体碰撞而耗尽其最后的生命值时停止运行。您在碰撞事件上添加断点,但是这么一来,每次碰撞都会导致运行停止。为了避免这种情况,您可以使用条件断点。
为了设置条件断点,您需要右击一个断点并为其添加一个条件。这里的条件可以是任何结果为 Boolean 的代码表达式。当代码运行到这一行时,如果表达式执行结果为 True,断点就会被激活。
这里,在玩家碰撞到一个物体的逻辑基础上,设置一个 player.health == 1 的条件,从而使您可以捕捉到玩家生命值降为 0 前的最后一次物体的碰撞事件。
依赖断点
一段代码会被不同的路径触发,这种情况在应用开发中并不少见。如果您发现了只会在某个特定路径才会触发的 Bug,随意为其打一个断点会造成许多无意义的运行中断。为了应对这种情况,您可以使用依赖断点。依赖断点只会在特定的断点被触发后才会激活。举例来说,您可以创建一个只会在您感兴趣的路径中被触发的断点,而其他断点便可以依赖此断点,从而使这些断点也只会在您所感兴趣的路径中被触发。
为了设置依赖断点,您需要右击路径中的第二个断点,并打开 More 菜单。在 Disable until breakpoint is hit 选框中,选中您想要依赖的断点:
您会发现断点的图标发生了改变:
现在,您的应用只会在前一个断点被触发后才会在此断点停止运行。
这个功能也可以用在其他使用了条件断点的地方,从而可以避免复制粘贴条件断点到新位置的操作。
挂起线程
如果您在调试一个多线程应用,您将会注意到,在默认情况下断点将会挂起所有线程,但有时您可能不希望它这样做。举例来说,您可能想要验证某个后台线程阻塞时,应用的其他功能是否能够正常工作,或者您希望了解在执行一个后台任务时,UI 能不能够持续进行渲染。
为了仅挂起当前线程,您需要打开断点选项,并且选中 Suspend 设置中的 Thread 选项:
Evaluate and log (评估与记录)
有些时候,相比起在断点处停止运行,您可能更希望看到一些有关应用状态的信息。也许您会通过在代码中添加 println 语句来做到这一点,但这种方法需要重新编译应用,您其实可以利用断点本身来进行评估与记录。
为了做到这点,您需要在断点选项中禁用 Suspend 并启用 Evaluate and log :
现在,您可以在输入框中添加任何代码表达式,相应内容会被评估并记录至控制台。
如果您只是想快速验证断点是否触发并且不在乎其中的细节信息,可以使用 "Breakpoint hit" 信息 来记录断点的触发事件。您甚至可以使用 Shift 添加断点的方式来让这一操作变得更加快捷。
禁用断点
禁用断点 (并非删除断点),可以右键点击断点并从弹框中取消选中 Enabled 选框。您也可以通过按住 Alt (在 Mac 上是 Option) 并点击断点,从而更快速地禁用断点。
断点分组
您一定遇到过这种场景: 您正在解决一个 Bug,并为此添加了几个断点,但是发现自己一时间没什么头绪,所以您就去解决别的 Bug 了。然而,很快您就开始触发为了解决第一个 Bug 所添加的断点。触发无关的断点不但会让人困扰,还会把您带离您的调试流程。
您可以使用断点分组来让开发过程更舒心一些。
当您的程序运行到第一个与当前调试流程无关的断点时,右击并打开 More 菜单,这时您会看到一个所有断点的列表,您可以在这里复选所有与第一个 Bug 相关的断点:
右击选中的断点,并选择 Move to group 接下来 Create new 并为新的分组命名,例如以您正在处理的 Bug 命名。现在,您可以仅通过点击轻松地启用和禁用所有的断点了。
当然,您也可以在解决了 Bug 之后,使用分组功能删除所有相关的断点。
Drop frame (丢弃当前帧)
有些时候,当您浏览挂起的代码时,可能会意外跳过某个本应该进入的方法。如果您的设备运行的是 Android 10 或者更高版本,您可以通过点击调试工具栏中的 Drop Frame 按钮来进行回溯:
这个功能会把您从当前的方法带回到其开始执行前的节点,从而给您一个重新进入该方法的机会。
此功能并不是 "时间机器"。如果您正处于一个长函数的中间位置,而它此前已经执行了许多工作 (例如,修改了当前类的状态)。在您丢弃当前帧时,这类操作所产生的改变不会被撤销。
Mark object (标记对象)
有时候,您会想要跟踪某些特定类型实例的生命周期。本例中,要跟踪的对象有一个哈希值: @10140:
为了能够在这个对象再次出现时可以认出它来,您可能已经掏出纸笔准备记下这个数字了。不过您也可以选择另一种方式: 右击该对象,点击 Mark Object 为其添加标签。
这样一来,无论被标记的对象出现在调试窗口的任何地方,它都会带有您添加的标签以方便辨认。这里我们为该对象添加一个 " myItem " 标签:
更棒的是,就算您处于完全不同的上下文,无法触及到刚才的对象,您也可以在 Watches 窗口对其进行查看。无论您处在什么位置,只要触发断点,就可以在 Watches 窗口添加后缀为 " _DebugLabel " 的标签 (不用担心自己会不记得后缀的内容,这里有自动补全):
现在,您可以在任何地方使用 Watches 窗口来观察该类型对象的状态。
您也可以将此功能与条件断点组合。举例来说,您可打一个断点,右击并为其设置一个条件来检查打了标签的对象:
这样一来,就不用在进入包含特定实例的范围之前跳过一堆断点,代码会运行到合适的地方再停止:
Evaluate expression (评估表达式)
尽管 Variables 和 Watches 窗口对于跟踪某个显式值时已经十分好用,但您有时候还是会想要更加自由地探索您的代码,这时候就轮到评估表达式功能登场了。当您正处于某个断点时,您可以使用调试工具栏中的 Evaluate expression 按钮来访问这一功能。
您可以在 Expression 输入框中输入任何表达式,点击 Evaluate 按钮就可以对其进行评估。当然,如果您评估了一个对象,在评估完成后,您就可以在 Result 部分浏览该对象的详细信息:
评估表达式弹窗可能会以单行模式打开,您可以通过点击 Expand 来将其扩展为多行模式:
现在,您可以输入复杂的多行表达式,其中可以包含变量、if 语句等各种内容:
Apply changes
前面讲过,当您使用条件断点时,会需要评估一个表达式;即便代码没有在断点停止,调试器依然需要执行评估操作。如果您在一个非常紧密的循环中运行评估操作,例如游戏中的动画处理,则可能导致应用停顿。尽管条件断点很有用,但在某些情况下您可能无法依靠它们。
解决此问题的一种方法是将条件表达式添加到代码中,并使用无操作 (no-op) 表达式,从而使其可以附加断点:
在修改完代码之后,您可能会决定重启应用并点击 Debug 按钮,但是如果您的应用运行在 Android 8 或更高版本的系统中,您可以使用 Apply Code Changes :
现在,嵌入的表达式已经修补了您的代码,但您仍然会在 Frames 窗口中看到您已经更新的方法被标记为 过时 (Obsolete) :
这是因为,虽然新的代码已经打好了补丁,但是调试器指向的仍是旧的代码。您可以使用丢弃当前帧的功能来离开旧的方法,然后进入修改过的方法之中。
尽管本例中并不需要,但这里还有一种选择: Apply Changes and Restart Activity 。不同于 Apply Code Changes ,该操作会重启 Activity。如果您修改了布局资源或您要调试的代码,例如在 onCreate 方法中,该选项将非常有用。
分析堆栈信息
就算您掌握了所有的窍门与技巧,您的代码仍然可能出现 Bug,而您也会因此收到一些崩溃报告,这些报告中则可能包含了异常堆栈信息的文本副本。您可以使用 Analyze 菜单中的 Analyze Stack Trace or Thread Dump 将这些信息转化为有意义的内容。
此工具提供了一个粘贴堆栈信息的文本框,不过它也会自动填充系统剪贴板中的文本:
点击 OK 之后,就会将包含完整注释的堆栈信息添加到控制台:
您可以一眼看出来自您自己代码文件的内容 (以蓝色突出显示) 与您可能不需要关注的代码 (以灰色突出显示)。并且,您可以通过单击链接在您的代码文件中进行跳转。
结语
本文提供了一些可以加快调试速度的技巧和窍门。由于篇幅所限,更多技巧简单归纳如下:
- 在 Debug 模式下,点击代码的行数数字可以直接执行此行代码
- Ctrl 拖动操作可以复制断点
- 您可以在函数的右括号处设置断点
- 您可以在字段和属性上设置断点,被称为 "field watchpoints"
- 您可以为接口中的方法设置断点,从而使它的所有实现都会触发断点
您可以通过下面这个视频了解更多本文的细节和演示内容:
- 腾讯视频链接: v.qq.com/x/page/o303…
- Bilibili 视频链接: www.bilibili.com/video/av781…
也请您查阅更多与本话题相关的资源:
- Android Developer 官方文档 | 调试预构建的 APK
- 通过数据浏览来控制数据在调试器中的显示方式
- 如何使用和理解 Overhead 选项卡
- Android Developer 官方文档 | Android Studio — 调试您的应用
- IntelliJ IDEA 调试代码