调试无响应的应用程序
阅读其他语言: English Español Português
市面上有很多关于调试器的教程,教你如何设置行断点、记录变量值或评估表达式。虽然这些知识本身为你提供了很多调试应用程序的工具,但现实世界中的场景可能会更复杂一些,需要更高级的方法。
![Post banner](/img/debug-unresponsive-apps-banner.png)
在本文中,我们将学习如何定位导致UI冻结的代码,而无需对项目有太多先验知识,并即时修复有问题的代码。
问题所在
如果你想跟着操作,请先克隆这个仓库: https://github.com/flounder4130/debugger-example
假设你有一个复杂的应用程序,在执行某些操作时会挂起。你知道如何复现这个错误,但困难在于你不知道哪部分代码负责这个功能。
![示例应用程序的界面有许多按钮可以执行一些操作](/img/sophisticated-app-1.png)
在我们的示例应用中,当点击Button N时会发生挂起。然而,要找到负责此动作的代码并不容易:
![在整个项目中搜索“按钮N”没有结果](/img/find-in-files.png)
![在整个项目中搜索“按钮N”没有结果](/img/find-in-files-dark.png)
让我们看看如何使用调试器来找到它。
方法断点
方法断点相比于行断点的优势在于,它们可以应用于整个类层次结构。这在我们的情况下有何帮助?
如果你查看示例项目,你会看到所有动作类都继承自带有单一方法 perform()
的 Action
接口。
![编辑器边距中的接口方法的断点图标](/img/method-breakpoint-in-interface.png)
![编辑器边距中的接口方法的断点图标](/img/method-breakpoint-in-interface-dark.png)
在这个接口方法中设置方法断点将在任何时候调用派生方法时暂停应用程序。要设置方法断点,点击声明该方法的行。
开始调试会话,然后点击Button N。应用程序将在 ActionImpl14
中暂停。现在我们知道与这个按钮相关的代码位于何处。
![应用程序在实现Action接口的类中暂停](/img/suspended-in-implementing-class.png)
![应用程序在实现Action接口的类中暂停](/img/suspended-in-implementing-class-dark.png)
虽然本文主要关注于发现错误,但这种方法在你想理解大型代码库中的某项功能是如何工作时,也能节省大量时间。
暂停应用程序
使用方法断点的方法效果很好,但它基于我们对父接口有所了解的假设。如果这个假设是错的,或者由于其他原因我们不能使用这种方法怎么办?
实际上,我们甚至可以在不使用断点的情况下做到这一点。点击Button N,当应用程序挂起时,转到IntelliJ IDEA。 从主菜单中选择运行 (Run) | 调试操作 (Debugging Actions) | 暂停程序 (Pause Program)。
![主线程的调用堆栈显示了当前正在做什么](/img/examine-threads-in-paused-app.png)
![主线程的调用堆栈显示了当前正在做什么](/img/examine-threads-in-paused-app-dark.png)
应用程序将被暂停,让我们能够检查线程和变量 (Threads & Variables)标签页中的当前线程状态。这让我们了解了应用程序当前正在做什么。因为它挂起了,我们可以识别出挂起的方法并追溯到调用位置。
这种方法比传统的线程转储有一些优势,我们很快就会讲到。例如,它以方便的形式提供变量信息,并允许你控制程序的进一步执行。
更多关于Pause Program的技巧和窍门,请参见暂停程序 (Pause Program):无需断点也可轻松调试!和Debugger.godMode()
线程转储
最后,我们可以使用线程转储,这严格来说不是调试器的功能。它在是否使用调试器之外都是可用的。
点击Button N。当应用程序挂起时,转到IntelliJ IDEA。 从主菜单中选择运行 (Run) | 调试操作 (Debugging Actions) | 获取线程转储 (Get Thread Dump)。
在左侧浏览可用的线程,在AWT-EventQueue中,你会看到是什么引起了问题。
![IntelliJ IDEA中的线程转储查看器](/img/ij-thread-dump.png)
![IntelliJ IDEA中的线程转储查看器](/img/ij-thread-dump-dark.png)
线程转储的缺点在于它们只提供了制作时程序状态的快照。你不能使用线程转储来探索变量或控制程序的执行。
在我们的例子中,我们不需要求助于线程转储。不过,我还是想提到这个技术,因为它在其他情况下可能有用,比如尝试调试没有使用调试代理启动的应用程序。
理解问题
无论使用哪种调试技术,我们都会到达 ActionImpl14
。在这个类中,有人打算在单独的线程中执行工作,但混淆了 Thread.start()
与 Thread.run()
,后者在调用代码的同一线程中运行代码。
IntelliJ IDEA的静态分析器甚至在设计时就警告我们关于这个可疑的调用:
![IntelliJ IDEA的静态分析给出了关于可疑调用Thread.run()的警告](/img/thread-run-warning.png)
![IntelliJ IDEA的静态分析给出了关于可疑调用Thread.run()的警告](/img/thread-run-warning-dark.png)
一个执行繁重任务(或者在这个案例中是长时间睡眠)的方法被调用在UI线程上,直到方法完成,才会阻塞它。这就是为什么我们在点击Button N后一段时间内无法在UI中做任何事情的原因。
HotSwap
现在我们已经发现了错误的原因,让我们修复这个问题。
我们本可以停止程序,重新编译代码,然后再运行它。然而,仅仅因为一个小改动就重新部署整个应用程序并不总是方便的。
让我们用聪明的方式来做。首先,使用建议的快速修复来更正代码:
![上下文菜单(Alt-Enter)给出选项以修复可疑代码](/img/quick-fix-replace-thread-run.png)
![上下文菜单(Alt-Enter)给出选项以修复可疑代码](/img/quick-fix-replace-thread-run-dark.png)
代码准备就绪后,点击运行 (Run) | 调试操作 (Debugging Actions) | 重新加载已更改的类 (Reload Changed Classes)。一个气球出现,确认新代码已进入VM。
![气球确认更新的类已经到达运行时](/img/hotswap-balloon.png)
![气球确认更新的类已经到达运行时](/img/hotswap-balloon-dark.png)
让我们回到应用程序并检查。再次点击Button N不再使应用程序挂起。
总结
通过我们的推理和几个调试器特性,我们能够定位到项目中导致UI冻结的代码。然后,我们没有浪费时间在重新编译和重新部署上就修复了代码,而在真实项目中这些步骤可能会非常耗时。
希望你发现描述的这些技术有所帮助。让我知道你的想法!
敬请期待更多内容!