圖片來自: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!