图片来自:https://101.dev/
本文作者:雪谷
前言
调试性能做为开发的必备神技,熟练掌握后能极大的进步开发效率,再也不用为频繁运行代码而苦恼了。文章同时还会具体介绍调试的原理以及一些调试过程中的常见问题,想晓得为什么办法断点那么慢?
接下来将从以下四个方面来解说调试是如何运作的:
- 调试操作
- 调试实战
- 调试原理
- 常见问题
调试简介
这里咱们介绍一下调试的常见操作,灵便把握这些操作,能够帮忙咱们疾速定位到对应代码或者获取想要的信息。
运行调试
开启 Debug 调试模式有两种形式:
Debug Run:间接以 Debug 模式运行 APP,该模式的长处是能够调试程序启动相干的代码,
例如 Application.onCreate()
。
Attach To Process:在程序运行中抉择过程来调试,该模式的长处是随时可开启、敞开 Debug 模式,应用灵便不便。
留神:Debug Run 会导致程序整体变慢,倡议应用期待调试,应用该形式能够在启动利用后处于期待状态,在开启调试后,利用才会走初始化流程,有两种形式开启期待断点:
办法 1:「开发者选项 – 抉择调试利用」的形式来调试利用启动阶段代码。具体形式为「抉择调试利用」->「运行利用」->「Attach To Process」,而后期待断点执行即可。
办法 2:应用 adb 命令 adb shell am set-debug-app -w --persistent 包名
开启,「-w」即示意利用启动时期待调试程序;敞开应用adb shell am clear-debug-app
。
调试操作
上面介绍一下 Debug 过程中的常见操作:
- Show Execution Point:跳到以后执行的断点处。
- Step Over:单步执行,执行到以后行的下一行。
- Step Into:进入正在执行的办法。
- Focus Step Into:同 3,然而能够进入源码,在 3 无奈进入的状况下,能够尝试该操作。
- Step Out:跳出正在执行的办法。
- Drop Frame:返回到以后办法的调用处。
- Run to Cursor:运行到光标处(光标必须在以后断点地位后)。
- Evaluate expression:计算选中的变量的值。
断点类型
断点分为以下四种类型:
行断点 :当执行到此行是进行执行,期待调试。
属性断点 :打在类的成员变量上,当变量初始化或变量的值扭转时触发断点。
异样断点 :当抛出指定异样时触发断点。
办法断点:当须要晓得一个办法的调用方时。
这里着重讲一下办法断点的应用场景:
如下所示,有个接口 IMethodTest
,同时有两个类 MethodTestImpl1
和 MethodTestImpl2
实现了该接口,在 IMethodTest
的 printMethod()
上打上办法断点。
在代码中实例化了 MethodTestImpl2
来调用 printMethod()
。
最初当 Debug 到该办法断点时,会主动走到 MethodTestImpl2
的 printMethod()
的实现中。
注:办法断点只反对 Java 代码。
调试实战
大家都晓得调试是进步开发效率的利器,那么它是如何帮忙开发者的呢?
答案就是「查看信息 」和「 缩小编译次数」。
查看信息
当程序运行后果并不如你的预期时,通过调试来查看以后内存里的变量以及堆栈信息,是最疾速定位问题的形式。
查看局部变量 的形式如下图所示
零碎主动打印:在以后调试地位之前的代码右侧会主动打印以后栈帧里保留的变量值。
鼠标悬停:鼠标悬停在一个变量上几秒后,会列出该变量的详细信息。
Variables 区:在 Variables 区里会主动打印以后办法里的变量详细信息。
查看全局变量 有两种形式
在 Variables 区增加监听:点击左侧操作栏里的「+」,输出对应变量值,即可实时察看该值的变动。
在 Evaluate Expression 中输出想要察看的变量,回车后即可查看以后时刻该变量的值。
注:查看局部变量和全局变量须要断点地位能拜访到该值。
查看堆栈信息
在调试页面的「Debugger」Tab 下能够查看以后的调用堆栈。
须要留神的是,一个线程只会被一个断点阻塞,然而不同线程是能够同时阻塞的,能够切换下拉框来切换线程,红色圆点示意正在被阻塞的线程。
缩小编译次数
越大的我的项目运行起来越是迟缓,而有时咱们只是批改了一行代码甚至是一个字符,这时再去从新编译是效率十分低下的,而灵活运用各种调试技巧,就能够帮忙咱们在不从新运行我的项目的前提下,去批改运行中代码。
运行期代码植入
想批改曾经运行起来的代码,有两种形式:
在 Variables 区中应用 setValue。
应用 Evaluate Expression。
Evaluate Expression 是一个十分弱小的性能,能够开展执行任意的代码段。灵活运用能够大量的缩小编译次数,例如:
- 批改网络申请、内部跳转等起源的数据,模仿各种场景。
- 执行某些代码,间接查看后果。
- 执行某一段异样代码,间接查看报错信息。
日志断点
日志是辅助开发排查问题的常见伎俩,然而在代码中增加日志存在一些不便的状况,例如:
- 须要从新运行程序。
- 开发实现之后须要去除对应的日志代码。
而应用日志断点就能够防止以上问题,应用形式为在断点地位右键,勾销 Suspend 框的勾选,同时勾选 Evaluate and Log 并输出想要的内容。
条件断点
当一个断点会被屡次执行,而调试时只需要在某些特定条件下才挂起,能够应用条件断点。应用形式为在断点地位右键,在 Condition 框中输出条件表达式,回车,这时断点右下角呈现一个「?」即为条件断点胜利挂载。
留神,条件断点的表达式返回值必须为 true 或者 false,否则断点报错。
异样断点
当开发者晓得接下来肯定会报某一个异样,然而又不晓得会是哪段代码触发时,能够尝试应用异样断点。应用形式为在断点治理界面点击「+」,增加 Java Exception Breakpoints。
而后输出你想要捕捉的异样,留神,这里也会捕捉零碎抛出的异样,捕捉时请仔细观察。
多线程断点
多线程是日常开发中常见的问题,针对一系列线程切换场景,调试工具也有对应的形式来辅助咱们定位问题。
这里请先思考一下这个示例,在不开启断点的状况下,下图的代码执行后会输入什么信息?
答案就是「无奈确定 」。
没错,在 CPU 的工夫片执行机制下,如果不加以控制,开发者是无奈预估线程执行程序的。而间接写一系列的线程控制代码耗时不小,有没有方法能先让线程依照开发者想要的程序去执行呢?请持续往下看:
在断点地位上右键,进去的治理界面里有 All 和 Thread 两个选项:
- All 示意阻塞所有线程,即所有线程都走到以后断点地位后,能力持续往下走。
- Thread 示意阻塞以后线程,即以后线程的代码走完后,才会走其余线程。
所以联合下面的示例:
All 选项的输入后果为:所有线程先执行完 start,再执行 end,然而哪个线程先执行无奈确定。
Thread 选项的输入后果为:一个线程先执行完 start,再执行 end,而后是另外一个线程,然而哪个线程先执行无奈确定。
调试 Release 包
调试 Relase 包偏 Android 逆向,因为篇幅无限,这里次要介绍和调试相干内容,后期筹备能够看这里 DebugApkSmali。
在反编译 APK,Smali 文件生成后,咱们须要把手机和 Android Studio 关联上,这里须要应用 Remote 性能,具体流程如下:
抉择 Edit Configurations。
新增 Remote JVM Debug,Name 随便,Port 不与现有端口抵触即可。
查看须要调试的页面位于哪个过程,先通过 adb shell dumpsys activity top | grep ACTIVITY
查看栈顶页面(这里调试的是知乎),而后在 AndroidManifest.xml 中查看对应 Activity 的 android:process
(没有该属性的话就看 application 的 process)。
通过 adb shell ps | grep com.zhihu.android
查看该过程对应的 PID,依据下图能够失去对应的 PID 为 16282。
最初通过 adb forward tcp:5005 jdwp:16282
连贯上手机和 Android Studio,就能够开始欢快的调试。
通过下面的介绍,咱们理解了调试 Release 包的形式,然而大家有没有一种雨里雾里的感觉呢,为什么晓得了端口就能够关联上?tcp 和 jdwp 又是什么意思?他们之前又是怎么传输数据的呢?带着这些疑难,咱们一起来看下调试原理。
调试原理
如果用简略的一句话来解释调试原理,能够概括为「通过 ADB 协定以及 JDWP 协定来实现调试器与虚拟机之间的通信」,如下图所示,调试的过程,其实就是通信的过程,了解了如何通信以及传递了那些信息,就明确了调试的外围原理。后续内容请都参考该图来了解。
ADB 架构
首先须要理解的是 ADB 架构,其中蕴含了三个局部:ADB Server、ADB Client 以及 ADB Dameon。
ADB Server
运行在电脑上的过程名为 adb 的后盾过程,端口号 5037,作用是治理 ADB Client 与 ADB Dameon 过程的通信。如下图所示,通过 adb device(任意 adb 命令均可)命令能够从常驻的后盾过程 adb 上 fork 一个子过程用于以后的通信。通过命令查看相干过程能够发现会有三个:
- Android Studio 过程连贯 adb 过程的通信。
- adb 过程连贯 Android Studio 过程的通信。
- adb 常驻过程。
ADB Server 中蕴含 Local Service 和 Remote Service,Local Service 用于与 ADB Client 交互,Remote Service 用于与 ADB Dameon 交互。
ADB Client
ADB Client 运行在电脑上,个别通过命令行或者 Android Studio 执行 adb 命令来与其交互。ADB Client 的主要职责是解析命令,做预处理,而后发送给 ADB Server,这里分为两种状况:
- ADB Server 能解决的命令就本人解决,如 adb version。
- ADB Server 不能解决的命令就发送给 ADB Dameon,并承受返回音讯,如 adb devices。
ADB Dameon
ADB Dameon 运行在手机上的服务过程,过程名为 adbd,在手机启动后,由 Zygote 过程创立。ADB Dameon 的主要职责是:
- 为手机提供 adb 服务。
- 创立 Local Service 和 Remote Service,Local Service 用于与 JVM 交互,Remote Service 用于与 ADB Server 交互。
理解了三者的分工后,能够通过下图对 ADB 架构有一个较为整体的了解。
看到这里,大家应该就能了解为什么连贯手机和 Android Studio 的命令是 adb forward tcp:5005 jdwp:16282
了,它实际上就是把 ADB 和 手机虚拟机进行连贯,同时也能够发现 ADB Server 和 ADB Dameon 之间的协定既能够是 USB(数据线)也能够是 TCP 的形式,其中 TCP 就是调试性能反对 WIFI、近程的根底。
注:因为篇幅无限,这里只对 ADB 架构做了简略的介绍,感兴趣的同学能够自行学习。
JDWP 协定
在理解了 ADB 协定后,咱们晓得了命令是如何从 Android Studio 或者命令行传输到手机上的 ADB Dameon 的,那么 ADB Dameon 又是如何与虚拟机交互的,以及传输协定中的数据格式又是怎么的呢,这里就须要了解 JDWP 协定了。
概念介绍
JDWP 是 Java Debug Wire Protocol 的缩写,其本质上是调试器和指标虚拟机进行调试交互的通信协议,通过命令包和回复包两种格局来传输数据。
这里有四个概念须要理解:
- 调试器(Debugger):Android Studio、Eclipse、DDMS、Terminal 等,他们都实现了反对 JDWP 通信接口。
- 指标虚拟机(Target VM):JVM、Art、Dalvik 等,在虚拟机启动时,会加载 JDWP 模块。
- 命令包(Command packet):调试器发送给虚拟机用于获取程序状态信息或管制程序运行,或者虚拟机发送给调试器用于告诉事件触发音讯。
- 回复包(Reply packet):虚拟机发送给调试器用于回复命令包的申请或者执行后果。
它们之间的交互如下图:
数据包
JDWP 数据包蕴含包头和数据两局部,数据局部就是简略的二进制数据流,咱们这里重视讲一下包头局部的构造,这也是调试命令传输的外围。
如上图所示,命令包和回复包的前三局部构造是雷同的:
- length:4 字节,数据包长度,蕴含包头和数据。
- id:4 字节,数据包序号,命令包和回复包必须保持一致。
- flags:1 字节,数据包类型,0x80 表示命令包,0x00 示意回复包。
不同之处在于最初 2 字节:
- 命令包蕴含 cmd set(命令分组)和 cmd id(命令序号)两局部,别离占 1 字节。
- 回复包里寄存的是 error code 错误码,非 0 即为存在谬误,占 2 字节。
常见的命令分组和序号依照性能大抵分为 18 组命令,蕴含了虚拟机信息、类、对象、线程、办法、事件等不同类型的操作命令。见下图:
该图片起源 FreeBuf。
查看残缺命令组及详细信息见:命令组。
这里以获取虚拟机版本的命令 VirtualMachine:version 为例演示,帮忙大家了解命令到底是如何传输的。
首先来看获取虚拟机版本会回复哪些信息:
通过上述表格能够推导出命令包与回复包的包信息为:
把对应编码转换成字符串为:
须要留神,非根本数据类型的内存构造,例如 String,应用「长度」+「字符数据」的模式。以 vmName 字段为例,DalvikVM 的 ASCII 码为「44 61 6c 76 69 6b 56 4d」,DalvikVM 的长度为 8,所以综合后 DalvikVM 的返回数据为「00 00 00 08 44 61 6c 76 69 6b 56 4d」。而 jdwpMajor 为纯数字,所以 jdwpMajor 的返回数据为「00 00 00 01」。
到这里调试原理就讲完了,原理局部只是从整体架构的层面为大家介绍了一下,外部还有很多的知识点值得大家去深究,感兴趣的同学能够自行学习。
常见问题
在讲完了调试实战和原理之后,咱们来看一些常见的调试问题:
- 断点被动断开
景象:在某些机型上,例如华为非鸿蒙零碎、局部 OPPO、一加设施等,当断点在 Activity、Fragment 的生命周期办法上超过 10 秒或者卡住页面展现超过肯定工夫(不同设施时长不统一)时,会呈现断点被动断开的状况。
解决形式:应用非阻塞式的日志断点。 - 无奈 Attach to Process
景象:在挂载过程进行调试时,呈现Error running 'Android Debugger (-1)': Invalid argument : Argument invalid [port]
的报错,这时是因为 adb 过程端口号被其余过程抢占了。
解决形式:应用adb kill-server
杀死 adb 过程,而后应用任意一个 adb 命令(adb devices)fork 一个新的 adb 过程即可。 - 办法断点导致 Debug 卡顿
景象:在应用办法断点时,调试器会变得异样卡顿,这是因为办法断点须要跟踪办法的入栈和出栈,每次进出都要发送指令给调试,具体流程如下:
1. 把办法断点退出断点列表。
2. 调试器发送指令通知虚拟机须要监听 Method Entry 和 Method Exit。
3. 虚拟机每次收到 Method Entry 或者 Method Exit 后发送事件给调试器。
4. 调试器判断是否在断点列表中。
5. 存在则向虚拟机发送 SetBreakPoint 申请挂起,否则发送申请开释该办法栈。
解决形式:
1. 依据理论状况放开 Method Entry 或者 Method Exit,如下图所示。
2. 用完即弃,及时去除办法断点。
3. 不要用!应用行断点(官网倡议)。
总结
调试是一个优良开发者必备的技巧,对晋升开发效率有极大的帮忙。把握调试原理也能够帮忙开发者更好的了解 Android 架构,是一个高级开发者的必经之路。
参考资料
- Android 调试桥 (adb)
- jdwp_handler
- JDWP 命令行调试
- Android 近程调试的摸索与实现
- Java Debug Wire Protocol Specification Details
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!