对于开发的同学来讲,Debug并不陌生,对于做Android开发的同学也是一样的。
可能有些同学会问,调试程序谁不会呢?的确,调试程序对现在的Android开发来说太常见了。我见过有些同学“熟练”的使用Log打印来看数据,不停的添加/删除Log,然后Run。这样有错吗?我想是没有的,但Android开发的同学都知道Run一次的时间是不短的。
也许很多同学会不削的说,我早就在使用Android Studio的Debug功能来调试程序了。的确,使用工具会为我们提供很多便利,也会为我们节省很多的时间。
但你真的了解Android Studio的调试工具的强大之所在吗?
你肯定遇到过,在断点的地方想查看其他变量的值,或者干脆想在断点的地方运算一段代码来了解当前条件下的结果。
你也肯定遇到过,想某一个断点只执行一次,然后自动的删除掉;或者干脆是某一个条件发生了再打开断点。
也许会想,在调试代码的时候有没有什么方法,当某一个异常抛出,或者被捕获的时候我能知道。
当然,你也有可能被多线程的调试困扰过,怎样才能只让目标线程执行断点而避开其他无关线程的干扰。
这些,Android Studio的Debugger已为你准备好。
为了更好的阅读本文,我们约定:
Debugger特指Android Studio Debugger
[10 表示代码的第10行
[Step Over] 表示Step Over这个功能
"Step Into" 表示Step Into这个菜单选项
文中所有快捷键使用Android Studio Default的Keymaps
1、一切从设置开始
有使用过Debugger经历的同学,对下图中<38>断点处的黄色字体不会陌生。细心的同学可能已经发现,<38>与<39>的debug信息与<37>的信息是不一样的,或者说与你平时看到的debug信息是不一样的。
在<38>的debug信息中,我们看到了自定义的信息: #LeiGuoting# id:4, createAt:0
这个自定义信息有时可以给我们带来很多方便,它可以很直观的给我们呈现出想要的信息,而不需要多余地操作。
那么这个自定义信息我们怎么做呢? 我们先把这个问题命名为: 问题1.1
在这个世界上,什么都不是单一存在的,有阴就有阳,有debug信息显示就有不显示的时候。
那么,怎么去关闭这个debug信息? 我们把这个问题命名为: 问题1.2
如果你对debugger的使用比较熟悉。 一定使用过F8执行[Step Over],一步一步的调试;
你也知道F7执行[Step Into]进入到一个方法继续调试。
在使用[Step Into]时,你肯定发现了,有时候按F7是“无效”的。
还有些方法,你是不用进入的,但你并不确定是否为这些方法,于是你还是F7了,进入该方法后,你发现你浪费了时间。
那么,上面的问题我们怎么去解决呢? 不急,暂时将这个问题命名为: 问题1.3
使用Android Studio的同学对于设置菜单是很熟悉的,但也许你忽视了下图中Debugger选项:
在这篇文章不会详细介绍设置。
Debugger分为三个子选项,"Data View", "Stepping", "HotSwap"。
"Data View" 中 "Show values inline" 能解决我们的问题1.2
问题1.1
我们需要使用到"Java Data Type Renderers"
点击左上角的绿色+创建一个新的Renderer
"Apply renderer to objects of type"中选择你要的class
"When rendering a node"中的"Use following expression"选择上面类的一个方法
问题1.3
在"Stepping"中,Debugger为我们做了一些默认的Skip设置,所以有些类我们F7是“无效”的。
如果有些类我们不想"Step Into",也可以在"Stepping"中添加。
Data View 负责Debugger显示相关的配置
Java
Java Data Type Renderers 自定义显示数据的渲染器,就像问题1.1中所看到的,我们可以自己定义需要显示的内容。
Stepping 对于Java代码,这项设置主要设置[Step Into]的功能,什么时候该跳过,什么时候该进入。
HotSwap Class Reload的一些策略。
2、断点(Breakpoint)
断点,不只一种。
Debugger为我们提供了5种断点类型,但我们使用的最多的是行断点(Line breakpoint)
Line breakpoint:当我们点击方法内一行代码左边的gutter时,我们设置的断点就是一个Line breakpoint,也是我们使用的最多的一种断点。
Temporary Line breakpoint:当我们设置一个断点为Line breakpoint时,将该断点的属性"Remove once hit"勾选,我们就设置了一个临时行断点。Line breakpoint如果不主动删除,这个断点就会一直存在;而一个Temporary Line breakpoint是临时的,使用一次后该断点就自动被删除。
Method breakpoint:当我们点击一个方法名左边的gutter时,生成的就是一个方法断点。Method breakpoint有两种触发方式,一是进入方法的时候,二是退出方法的时候,两种可以同时存在。
Java Exception breakpoint:根据我目前的了解,异常断点只能通过[View Breakpoints]来打开断点管理器创建一个该类型的断点。 顾名思义,该断点是用来调试跟踪Java异常使用的,当其他条件都满足的情况下,如果我们勾选了"Catch exception"选项时,只有我们catch了某个类型的异常时触发该断点。 如果我们勾选了"Uncatch exception"选项时,只有我们没有catch了某个类型的异常时触发该断点。
Field watchpoint:这是一个可以随时观察变量的断点。当我们点击一个共享变量的左边gutter时,我们就会设置一个该类型的断点。只要这个变量有变化都会触发该断点。
在Android Studio中,5种断点都有不同的小图标对应来标识具体的类型。并且,断点的不同状态,也是有不同的小图标来标识的。这样我们就很容易记住他们。
(断点的类型图标和状态图标,图片来源:https://www.jetbrains.com))
上图中我们可以很清晰的看到不同断点对应的图标,还有他们各自不同状态时对应的图标。
在实际使用的时候,我们就可以很容易,也很清晰的分辨出断点的类型及状态,方便我们的使用。
断点的前4状态我们是很容易理解的,最后一种Conditionally disabled我们在后面会介绍。
如果说断点仅仅只有这5种状态,那么它的功能是很有限的,但现实是Debugger为我们提供了很强大的功能。
它有Actions,也有Filter。这两种的组合可以为我们提供很强大的功能了。
[View Breakpoints],可以打开如下图的一个菜单:
我们在断点红色小圆点上右键,我们会看到一个弹出框,上面有简单的enable,Suspend的策略选项。如果我们继续点击"More"也会看到上面的图。
Enabled:开启/关闭断点
Suspend:如果这个checkbox未勾选,程序执行到这一行时不会暂停
Condition:条件,如果该条件为true时才会暂停该断点,如果条件不满足时是不会暂停的
Log message to console:开启将log打印到Debugger的console
Log evaluated expression:我们可以使用这个选项填写自己想要输出的信息到console,这样我们可以在代码运行的时候通过该选项动态的修改输出的信息。
Disabled until selected breakpoint hit:断点与断点之间的关联,我们可以在下拉列表中选中一个断点A,当这个A断点被触发的时,该断点执行"Disable again"或者"Leave enable"
Filters:
Instance filters 根据给定的对象ID来过滤
Class filters 根据class条件来过滤
Pass count 跳过的次数;比如,这个值为2,那么该断点会跳过2次后才会触发
3、调试(Stepping)
Step Over:执行下一行语句(下一行语句不需要打断点)
Step Into:当前Suspended的语句是一个方法(非Skip方法),进入到该方法内(该方法内不需要断点)
Force Step Into:当前Suspended的语句是一个方法(包括Skip的方法),强行进入到该方法内(该方法内不需要断点)
Step Out:退出当前方法
Run to Cursor:跳转到光标所在的行
Force Run to Cursor:强行跳转到光标所在的行。如果当前断点与所在光标之间还有其他断点,那么我们执行[Run to Cursor]时是不会跳转到光标所在行的,而是到下一个断点处;而如果执行的是该命令,则会直接跳转到光标所在行。
下图中右上角一行的几个蓝色和红色箭头分别对应着这几个Step,当然在这个图中是没有"Force Run to Cursor"。在我们Android Studio菜单"Run"里面包含了所以的Step。
4、Debugger窗口
当我们启动一个Debugger Session后,会看到下面的一个窗口。
1)Debugger debugger的主窗口。在设置一节中我们讲到了Data View设置,它所对应的部分显示效果就在这个窗口中提现出来。
Frames:所有Frame的列表,Frame体现一个线程的堆栈信息。
Threads:所有线程列表,其中我们可以看见线程的状态。并且在该列表中线程只有三种状态,RUNNING, WAIT, UNKNOWN
Variables:变量池, 显示当前断点对应的上下文的变量。在这个窗口中,我们可以选中某个变量,右键我们可以修改该变量的值。还可以[Inspect]打开变量的监视窗口,这样就方便我们对变量的观察了。
Watches:我们可以通过Watches窗口的绿色+号来创建一个需要观察的变量。 这样我们就能很直观的在这个窗口看到该变量的变化,而无需去跟踪代码。
2)Console 我们在讲断点属性的时候知道,我们可以开启或者关闭某一个断点的log,还可以自定义需要输出到控制台的log信息,方便信息的输出,而不必去修改代码。
3)Favorites 在该窗口中我们可以看到所有的断点。
使用Java Exception Breakpoint
创建一个"Uncaught Exception"类型的Java Exception Breakpoint,如下图
在断点那一节我们知道,创建一个异常断点,只能通过这个界面左上角的绿色+号来创建。
"Uncaught Exception"类型的异常断点,在NullPointerException异常被抛出并且没有被catch时会被触发。
启动Debugger Session,如下图:
F9 Resume程序,结果如下图:
这时, 异常断点被触发。
6、断点的日志输出
通过断点日志,我们可以很方便的修改想要看到的日志。
创建输出到Console的日志:
执行到这个断点时,发现控制台信息如下:
我们修改下日志信息:
执行时的输出的结果为:
7、断点的关联
在有些时候,我们希望一个断点的开启与关闭可以依赖于另外一个断点是否被触发。如果遇到这种情况,我们就可以使用在断点那一节介绍的"Disabled until selected breakpoint hit"选项。
开启一个断点依赖于另一个断点:
<38>断点的意思是,这个断点默认是disabled状态直到<49>断点被触发。当<49>断点被触发的时候,<38>断点状态为enable。下一次,<38>断点被触发后,该断点再一次disable, 因为我们选择的是"disable again"。
开启Debug Session后的初始状态:
未达到条件触发<49>之前的状态:
第一次达到条件并触发<49>时的状态:
这一趟debug后,下一趟debug时,<38>断点就可以被触发了,当它被触发后,再将状态自动变为disabled状态。
8、多线程调试
有段时间,我是多么的希望能利用Debugger快速有效地对目标线程就行调试。当时对多线程的调试束手无策,只能使用Log,但效率太低了。
多线程的调试需要依赖于在断点那一节讲到的断点Actions或者Filters,通过这些条件我们可以找到我们需要的线程来调试。
先定义一个线程类:
public class MultiThread extends Thread {
private final MetaInfo metaInfo;
public MultiThread() {
metaInfo = new MetaInfo();
setName(String.format("MultiThread$%s", metaInfo.getId()));
}
@Override
public void run() {
checkNull(metaInfo);
String thread = Thread.currentThread().getName();
Log.d(TAG, String.format("#MultiThread# thread:%s", thread));
metaInfo.setCreateAt(System.currentTimeMillis());
Log.i(TAG, String.format("#MultiThread# thread:%s, id:%s, createAt:%s",
thread, metaInfo.getId(), metaInfo.getCreateAt()));
}
}
设置断点
断点条件为:0 == metaInfo.getId() % 3 (MetaInfo的id是从1开始自增的)
开始调试
Debugger按照我们给定的条件筛选出了我们想要的线程。
在多线程调试中,有时我们可能筛选出来的不是一个线程,而是一组。那么,
可以使用"Frames"的下拉列表来切换线程调试。
文末小结
Android Studio Debugger的功能是很强大的,好好利用起来,可以提高我们的工作效率,因为Android开发的童鞋都知道,重新Run一次的时间是不短的。
Debugger功能的强大,还体现在这里没有详细介绍的"Inspect", "Evaluate Expression"等上面。
"Evaluate Expression" 可以很方便的查看各种相关的值,包括计算在内的。
"Variables"中的[Set Value]可以修改成任意我们想要的值来达到测试各种case的情况,有时可以方便的制造点Mock数据。
对于Debugger,我们能利用的有很多很多,不要让它成为我们的忽视或者轻视的对象。
本文作者:雷国廷(点融黑帮),现任职点融Mobile团队,Android开发工程师。热爱生活,也热爱代码。