概述
在Android開(kāi)發(fā)過(guò)程中,調(diào)試是不可避免的,在IDE的幫助下,只需要在IDE按鈕上點(diǎn)擊兩下便可以進(jìn)行調(diào)試。這讓調(diào)試的工作變得十分簡(jiǎn)單方便,以至于開(kāi)發(fā)者只需要熟記各種IDE的debug技巧,無(wú)需了解調(diào)試原理就可以完成程序的debug。
在調(diào)試的時(shí)候,開(kāi)發(fā)者可以打斷點(diǎn)調(diào)試、需改運(yùn)行參數(shù)、dump虛擬機(jī)的堆棧信息、遠(yuǎn)程調(diào)試等,那這些都是怎么做到的呢?本文將帶你一起探討 Android 的調(diào)試原理。
要學(xué)習(xí) Adb 的調(diào)試原理,需要從稍微簡(jiǎn)單一點(diǎn)的 Java 調(diào)試原理入手,因此首先介紹一下 Java 調(diào)試原理。
手動(dòng)調(diào)試Java
在正式介紹Java的調(diào)試原理前,首先進(jìn)行一次手動(dòng)的 Java 程序調(diào)試。
第一步,編寫(xiě) java 文件:
public class TestMain {
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(1000);
String hello = hello("" + new Random().nextInt(100));
System.out.println(hello);
}
}
private static String hello(String hello) {
return hello;
}
}
第二步,將 java 文件,編譯 class 文件:
$javac -g src/com/example/www/TestMain.java -d class
第三步,使用debug模式,運(yùn)行 class 文件并監(jiān)聽(tīng)8000端口,掛載 jdwp:
$java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 -cp class/ com.example.www.TestMain
第四步,使用調(diào)試工具 jdb 與 8000 端口進(jìn)行通訊,開(kāi)始調(diào)試:
$jdb -attach localhost:8000
第五步,在 TestMain.java的第十行上打斷個(gè)點(diǎn):
> stop at com.example.www.TestMain:10
效果如下:
sxxxx0@wxxxxxeMBP test % jdb -attach localhost:8000
設(shè)置未捕獲的java.lang.Throwable
設(shè)置延遲的未捕獲的java.lang.Throwable
正在初始化jdb...
> stop at com.example.www.TestMain:10
設(shè)置斷點(diǎn)com.example.www.TestMain:10
>
斷點(diǎn)命中: "線程=main", com.example.www.TestMain.main(), 行=10 bci=6
main[1] run
>
斷點(diǎn)命中: "線程=main", com.example.www.TestMain.main(), 行=10 bci=6
main[1] clear com.example.www.TestMain:10
已刪除: 斷點(diǎn)com.example.www.TestMain:10
main[1] stop in com.example.www.TestMain.hello
設(shè)置斷點(diǎn)com.example.www.TestMain.hello
main[1] run
>
斷點(diǎn)命中: "線程=main", com.example.www.TestMain.hello(), 行=16 bci=0
main[1]
至此,手動(dòng)調(diào)試已開(kāi)啟,并讓調(diào)試過(guò)程停留在了 TestMain#hello 方法上(源碼的第16行)。除了 stop at 可以打行斷點(diǎn)外,如上,還可以通過(guò) stop in 打上方法斷點(diǎn)。同時(shí),還可以使用 -source 指定源碼路徑,IDE默認(rèn)設(shè)置的源碼路徑則是 $PROJECT_ROOT(項(xiàng)目根路徑)。 打開(kāi) debug config ,可以在IDE中手動(dòng)設(shè)置源碼路徑,告訴jdb該去哪里找到源碼:
如果想了解更多的 java debug 指令可查閱官方文檔。
在上面五步的調(diào)試中,從第三步開(kāi)始,可能大部分讀者就比較生疏了,因?yàn)樵谡{(diào)試程序調(diào)試時(shí),按下IDE的 debug 按鍵后,IDE就在后臺(tái)自動(dòng)運(yùn)行了該 class 文件,并且使用 jdb 幫我們將界面上的埋點(diǎn)轉(zhuǎn)化為埋點(diǎn)指令,無(wú)需開(kāi)發(fā)者手動(dòng)調(diào)試。
現(xiàn)在,打開(kāi) Debug 后在 Console 這里輸出的這些提示大概可以理解了吧 :-)
呃呃,這 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n
是什么玩意 ,還是有點(diǎn)懵:-) ,讀完這篇文章后你就能知道這些具體是什么回事啦~
JDPA
下面一起看看 JVM 的調(diào)試原理吧!Java 的調(diào)試體系(Java Platform Debugger Architecture,以下簡(jiǎn)稱(chēng):JDPA)定義了 JVM 調(diào)試的過(guò)程,學(xué)習(xí) JVM 的調(diào)試原理,實(shí)際上就是學(xué)習(xí)JDPA。
如上圖,在JDPA中包含三個(gè)部分:
- Java 虛擬機(jī)工具接口(JVMTI) : 可以通過(guò)『實(shí)現(xiàn)JVMTI可以獲取、控制被調(diào)試虛擬機(jī)的運(yùn)行狀態(tài)』,JVMTI是調(diào)試的基礎(chǔ),JVMTI由JVM自身提供
- Java 調(diào)試線協(xié)議(JDWP) : The Java Debug Wire Protocol (以下簡(jiǎn)稱(chēng):JDWP)定義了調(diào)試者(Debugger)和被調(diào)試者(Debuggee)通訊協(xié)議
- Java 調(diào)試接口(JDI) : 調(diào)試者通過(guò)實(shí)現(xiàn) JDI ,調(diào)試者可以向JVM發(fā)送調(diào)試命令, 接受JVM運(yùn)行時(shí)的狀態(tài)信息(例如:jdb是JDI的一個(gè)實(shí)現(xiàn))
調(diào)試的本質(zhì)就是 Debugger 與 Debuggee 的之間的通訊,JDWP 則是通訊所用的協(xié)議。
JDPA工作流程
下圖描述了JDPA工作流程:
- Debugger
直接或者間接實(shí)現(xiàn) JDI ,并使用 JDWP 定義的通訊規(guī)則發(fā)送或者接受來(lái)自 JDWP 的數(shù)據(jù)與命令。例如:JDB ,IDE 自帶的調(diào)試工具。
- JDWP Agent
JJVMTI 的具體實(shí)現(xiàn),開(kāi)發(fā)時(shí)一般采用建立一個(gè) Agent 的方式來(lái)使用 JVMTI,它使用 JVMTI 函數(shù),設(shè)置一些回調(diào)函數(shù),『接受或發(fā)送來(lái)自 Debugger 的數(shù)據(jù)與命令,并通過(guò)這些數(shù)據(jù)與命令去獲取或者操作被調(diào)試虛擬機(jī)的運(yùn)行狀態(tài)』。
在Java 虛擬機(jī)啟動(dòng)時(shí)可以選擇加載的 JDWP Agent ,例如,進(jìn)行遠(yuǎn)程調(diào)試,我們需要指定加載jdwp:
$java -agentlib:jdwp=transport=dt_socket
上述參數(shù),不僅指定需要加載 JDWP Agent ,并且指定了 JDWP Agent 與 Debugger 間使用socket進(jìn)行通訊。
- JDWP
JDWP 規(guī)定了 JDI 與 JVMTI 之間的通訊協(xié)議,JDWP 并不包含傳輸層的實(shí)現(xiàn),因此 JDWP 數(shù)據(jù)可以使用任意的傳輸方式傳輸,只需要數(shù)據(jù)格式滿(mǎn)足 JDWP 所規(guī)定的格式即可。
- Target JVM
被調(diào)試的虛擬機(jī)。
JDWP協(xié)議
和Http協(xié)議一樣,JDWP協(xié)議同樣有握手和應(yīng)答。
通訊握手
JDWP協(xié)議的通訊過(guò)程,由一個(gè)簡(jiǎn)單的握手開(kāi)始,如下圖所示:
Debugger 發(fā)送字符串”JDWP-Handshake”到 Target Java 虛擬機(jī)
Target Java 虛擬機(jī)回復(fù)”JDWP-Handshake”,握手成功
在 Target JVM 啟動(dòng)時(shí),可以選擇『監(jiān)聽(tīng)指定調(diào)試端口』也可以將自己『直接連接到已有的調(diào)試端口』上去。再來(lái)看看上面那條長(zhǎng)長(zhǎng)的指令
$java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 -cp class/ com.example.www.TestMain
server
參數(shù)用來(lái)控制啟動(dòng)選項(xiàng),y
表示監(jiān)聽(tīng)指定『調(diào)試端口』,n
則表示連接到已有的『調(diào)試端口』。
suspend
參數(shù)則是是否在調(diào)試開(kāi)始前暫停虛擬機(jī)。
所以,上述的指令表達(dá)的意思為:
運(yùn)行 TestMain.class
,并且監(jiān)聽(tīng)8000這個(gè)調(diào)試端口,當(dāng)外部(例如:jdb)向此端口發(fā)送一個(gè)"JDWP-Handshake"時(shí),就表示對(duì)方在請(qǐng)求作為當(dāng)前運(yùn)行的虛擬機(jī)的調(diào)試端。希望了解更多相關(guān)內(nèi)容的同學(xué),可以參考官方文檔
通訊數(shù)據(jù)包
握手完成后,debugger 就可以與 target Java 虛擬機(jī)相互發(fā)送數(shù)據(jù)了。JDWP中的數(shù)據(jù)包分為兩種:命令數(shù)據(jù)包(CmdPacket)、回復(fù)數(shù)據(jù)包(ReplyPacket)。
CmdPacket
首先看 CmdPacket 的結(jié)構(gòu):
typedef struct {
jint len; // packet length
jint id; // packet id
jbyte flags; // value is 0
jbyte cmdSet; // command set
jbyte cmd; // command in specific command set
jbyte *data; // data carried by packet
} jdwpCmdPacket;
- Length :是整個(gè)packet的長(zhǎng)度,包括 length 部分。因?yàn)榘^的長(zhǎng)度是固定的11bytes,所以如果一個(gè)command packet沒(méi)有數(shù)據(jù)部分,則length的值就是11。
- Id :是一個(gè)唯一值,用來(lái)標(biāo)記和識(shí)別reply所屬的command。Reply packet與它所回復(fù)的command packet具有相同的Id,異步的消息就是通過(guò)Id來(lái)配對(duì)識(shí)別的。
- Flags :目前對(duì)于command packet值始終是 0。
- Command Set :相當(dāng)于一個(gè)command的分組,一些功能相近的command被分在同一個(gè)Command Set 中。
Command Set的值被劃分為 3個(gè)部分:
0-63: 從 debugger 發(fā)往 target Java 虛擬機(jī)的命令
64 – 127: 從 target Java 虛擬機(jī)發(fā)往 debugger 的命令
128 – 256: 預(yù)留的自定義和擴(kuò)展命令
ReplyPacket
再看看 ReplyPacket :
typedef struct {
jint len; // packet length
jint id; // packet id
jbyte flags; // value 0x80
jshort errorCode; // error code
jbyte *data; // data carried by packet
} jdwpReplyPacket;
Flags : 目前對(duì)于 reply packet 值始終是0x80。我們可以通過(guò) Flags 的值來(lái)判斷接收到的packet 是 command 還是 reply 。
Error Code : 用來(lái)表示被回復(fù)的命令是否被正確執(zhí)行了。零表示正確,非零表示執(zhí)行錯(cuò)誤。
Data : 內(nèi)容和結(jié)構(gòu)依據(jù)不同的 command 和 reply 都有所不同。比如請(qǐng)求一個(gè)對(duì)象成員變量值的 command ,它的 data 中就包含該對(duì)象的 id 和成員變量的 id 。而 reply 中則包含該成員變量的值。
讀者如果希望更深入了解JDWP 協(xié)議,推薦閱讀:JDWP 協(xié)議及實(shí)現(xiàn)。
Android 調(diào)試原理
分析完了 Java 的調(diào)試原理,下面接著分析Android 調(diào)試原理 。Android的調(diào)試原理與Java的調(diào)試原理相比,要稍微復(fù)雜一些。
上圖是adb的結(jié)構(gòu)圖:
Host 為PC端,在PC端運(yùn)行著 Adb server 與 Adb clients, 同時(shí)運(yùn)行著手機(jī)模擬器( Emulator )。
Target device 為手機(jī), 無(wú)論是手機(jī)或者是手機(jī)模擬器,都運(yùn)行著 Adbd (Adb daemon)和虛擬機(jī)(黃色的橢圓)
在Java的調(diào)試中,JDI 與 JVMTI 之間使用 JDWP 協(xié)議通訊來(lái)完成調(diào)試工作。在 Android 的調(diào)試>中,Adbd 與 虛擬機(jī)也是采用 JDWP 進(jìn)行通訊,所以 ”Adbd 同 jdb 類(lèi)似都是 JDI 的具體實(shí)現(xiàn)“。
構(gòu)成介紹
Adb server 啟動(dòng)以后會(huì)一直監(jiān)聽(tīng)本地的 5037 端口。adb client 通過(guò)本地的隨機(jī)端口與 5037 端口建立連接。一個(gè)PC可以連接多臺(tái)手機(jī)設(shè)備或虛擬機(jī),一個(gè)手機(jī)也可以同時(shí)連接多臺(tái)PC,這些設(shè)備的連接管理由 Adb server 完成。
Adb clients 可視為一個(gè)shell窗口(當(dāng)使用 adb shell 命令時(shí),可創(chuàng)建一個(gè)客戶(hù)端)。當(dāng)在執(zhí)行輸入 adb shell命令時(shí),客戶(hù)端會(huì)開(kāi)啟一個(gè)隨機(jī)端口去與 5037 端口進(jìn)行通訊,完成連接本地的服務(wù)端程序。如果 Adb server 沒(méi)有啟動(dòng),則啟動(dòng)一 Adb server 服務(wù)端程序。
如圖,在運(yùn)行 adb devices 命令時(shí)啟動(dòng)了 Adb server ,并開(kāi)始監(jiān)聽(tīng) 5037 端口,當(dāng)運(yùn)行 adb shell 命令后,再查看端口占用情況,可以看到 5037 端口與 53094 端口建立了連接,當(dāng)關(guān)閉 adb shell 窗口后, 53094 端口關(guān)閉,這個(gè) 53094 端口即為上面所說(shuō)的 Adb Client 產(chǎn)生的隨機(jī)端口。
Adb daemon(adbd) 在模擬器或移動(dòng)設(shè)備上運(yùn)行的后臺(tái)服務(wù)。當(dāng) Android 系統(tǒng)起機(jī)的時(shí)候,由 init 程序啟動(dòng) adbd 。如果 adbd 掛了,則 adbd會(huì)由 init 重新啟動(dòng)。換言之,只要 Android 系統(tǒng)在運(yùn)行,那 adbd 就是“不死的”,常年在伺服狀態(tài)
通訊介紹
- Adb clients 采用特定的格式的數(shù)據(jù)向 Adb server 發(fā)送各類(lèi)命令
這些數(shù)據(jù)的格式為:Length(4字節(jié)) + commend
例如: 000Chost:version 中 000C 表示命令長(zhǎng)度,實(shí)際命令為 host:version
Server收到Client的請(qǐng)求后,返回的數(shù)據(jù)遵循如下格式:
如果成功,則返回四個(gè)字節(jié)的字符串”O(jiān)KAY“
如果失敗,則返回四個(gè)字節(jié)的字符串”FAIL“和出錯(cuò)原因
如果異常,則返回錯(cuò)誤碼
當(dāng) Adb Client 發(fā)送命令并收到 Adb Server 返回的“OKAY”回復(fù)后,就可以繼續(xù)發(fā)起操作命令了。
- Adb Server 與 Adb daemon 之間采用的是 [『transport 協(xié)議』], Adb daemon 在手機(jī)(或模擬器)上啟動(dòng)后將一直監(jiān)聽(tīng)手機(jī)(或模擬器)的 5555 端口。Adb Server啟動(dòng)后會(huì)試圖與 5555 端口進(jìn)行通訊,通訊采用無(wú)線(TCP協(xié)議)或者USB完成傳輸數(shù)據(jù)。
總結(jié)
至此 Android的調(diào)試原理介紹完成,在調(diào)試過(guò)程中,需要值得注意的是:
- 開(kāi)發(fā)可以設(shè)置斷點(diǎn)位置外,還可設(shè)置源碼路徑。因此,在調(diào)試的時(shí)候運(yùn)行的程序可以不是實(shí)際的源代碼,只要 斷點(diǎn)信息 相同,就可以進(jìn)行調(diào)試。
- 斷點(diǎn)信息:行斷點(diǎn)的信息(由包名、類(lèi)名、行號(hào)組成);方法斷點(diǎn)的信息(由包名、類(lèi)名、方法名組成)
-
方法斷點(diǎn)相對(duì)于行斷點(diǎn)比較重量級(jí),在調(diào)試的時(shí)候如果發(fā)現(xiàn)程序運(yùn)行非常緩慢甚至無(wú)響應(yīng),可以去掉所有的方法斷點(diǎn), 效果立竿見(jiàn)影。(如下圖中的 Java Method Breakpoints)
image
(文章中的錯(cuò)誤與不足之處還請(qǐng)廣大讀者評(píng)論留言,一起討論,一起進(jìn)步:-)
推薦閱讀
Java Platform Debugger Architecture
Java Platform, Standard Edition Tools Reference
Java: slow performance or hangups when starting debugger and stepping
adb原理
JDWP 協(xié)議及實(shí)現(xiàn)
JPDA 體系概覽
Android虛擬機(jī)調(diào)試器原理與實(shí)現(xiàn)
【Android】ADB工具原理探究
adb client, adb server, adbd原理淺析
Android中ADB-server、ADB-client和adbd的簡(jiǎn)介