1. 程式人生 > >iOS除錯-lldb學習總結

iOS除錯-lldb學習總結

Xcode的使用中總是離不開除錯這個環境,在一年多的iOS開發時間中,我更多地依賴於XCode本身提供的GUI工具來進行除錯,而對LLDB敬而遠之,這段時間好好學習了LLDB的使用,發覺我錯過了太多東西了……因此做一個比較完備的總結,也希望在寫這篇文章的過程中進一步學習LLDB除錯的各種實踐方法。

LLDB闡述

LLDB 是一個有著 REPL 的特性和 C++ ,Python 外掛的開源偵錯程式。LLDB 繫結在 Xcode 內部,存在於主視窗底部的控制檯中。偵錯程式允許你在程式執行的特定時暫停它,你可以檢視變數的值,執行自定的指令,並且按照你所認為合適的步驟來操作程式的進展。

它的基本語法為

<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]

在XCode中,我們需要在程式暫停進入除錯狀態時才能使用LLDB,可以通過breakpoint、watchpoint、或者XCode的除錯臺自帶的暫停按鈕使程式暫停。

HELP

LLDB的入門與Linux命令入門類似,可以通過執行help命令來查詢這個命令的意義和詳細引數

從描述中我們能看出thread backtrace是用來查詢暫停時的執行緒堆疊的,並瞭解了可以帶入的入參。

唯一匹配原則

LLDB有個很省事的特性,如果輸入的字母已經能匹配到某個命令,就可以直接執行,等於輸入了完整的命令。

可以看到expressione是等價的。

變數查詢與修改

1. expression

expression可簡寫為e,作用為執行一個表示式,首當其衝,它肯定可以用來查詢當前堆疊變數的值。

當然e的更主要的用法是通過執行表示式,動態修改當前執行緒堆疊變數的值,從而達到除錯的目的(其實查詢也很主要,只是會用另一種方式查詢)。
比如,我們可以在某個if..else..的語句前打上斷點,直接修改條件表示式的值,使程式覆蓋了不同分支,而不用苦心積慮地停止程式、hard code變數值來進行除錯,節省了一大坨修改與編譯時間。

在上面這份測試程式碼,在進入條件判斷語句前打了斷點,那我們可以通過e命令,來自由控制程式走向任何一個分支。

我們也可以通過執行表示式,實時改變當前的UI介面,方便介面程式碼的除錯,比如我們可以執行下面程式碼來改變當前UI,讓cellItem的邊框顯示出來,以判斷我們的介面佈局是否正確。
e @import UIKit
e cellItem.layer.borderWidth = 1
這裡有個特殊的問題,由於程式已經被斷點暫停了,因此執行UI更新的執行緒也被暫停了。我們可以通過讓程式繼續執行,也可以通過另一條表示式來更新UI。
e (void)[CATransaction flush]

2. p、po

在上面說過,在除錯中,我們一般用e命令來修改變數,而查詢變數一般用ppo命令。
po的作用為列印物件,事實上,我們可以通過help po得知,poexpression -O --的簡寫,我們可以通過它打印出物件,而不是列印物件的指標。而值得一提的是,在 help expression 返回的幫助資訊中,我們可以知道,po命令會嘗試呼叫物件的 description 方法來取得物件資訊,因此我們也可以過載某個物件的description方法,使我們除錯的時候能獲得可讀性更強,更全面的資訊。

-(NSString*)description
{
    return [NSString stringWithFormat:@"Portal[%@, %@, %@, %@, %@, %@, %@]", ssid, mpUrl, ticket, authUrl, _openid, _tid, extend];
}

p即是print,也是expression --的縮寫,與po不同,它不會打出物件的詳細資訊,只會打印出一個$符號,數字,再加上一段地址資訊。由於po命令下,物件的description 有可能被隨便亂改,沒有輸出地址訊息。

$符號在LLDB中代表著變數的分配。每次使用p後,會自動為你分配一個變數,後面再次想使用這個變數時,就可以直接使用。我們可以直接使用這個地址做一些轉換,獲取物件的資訊

斷點

所有除錯都是由斷點開始的,我們接觸的最多,就是以breakpoint命令為基礎的斷點。
一般我們對breakpoint命令使用得不多,而是在XCode的GUI介面中直接新增斷點。除了直接觸發程式暫停供除錯外,我們可以進行進一步的配置。

  • 新增condition,一般用於多次呼叫的函式或者循壞的程式碼中,在作用域內達到某個條件,才會觸發程式暫停
  • 忽略次數,這個很容易理解,在忽略觸發幾次後再觸發暫停
  • 新增Action,為這個斷點新增子命令、指令碼、shell命令、聲效(有個毛線用)等Action,我的理解是一個指令碼化的功能,我們可以在斷點的基礎上新增一些方便除錯的指令碼,提高除錯效率。
  • 自動繼續,配合上面的新增Action,我們就可以不用一次又一次的暫停程式進行除錯來查詢某些值(大型程式中斷一次還是會有卡頓),直接用Action將需要的資訊列印在控制檯,一次性檢視即可。

除去在程式碼中直接點選新增斷點外,我們也可以在 command + 7 breakpoint頁面下直接新增相關的斷點。我們常用的有 Exception Breakpoint 與 Symbolic Breakpoint

  • Add Exception Breakpoint
    Exception Breakpoint為異常斷點。在某些情況下,TableView的資料來源與UI操作不一致,或者容器插入了nil的指標,將訊息傳至野指標,都會導致程式的crash,並且LLDB輸出的資訊不是很友好。加上異常斷點,能夠使程式在丟擲異常的棧自動暫停,可直接定位導致丟擲異常的程式碼。在一般的開發流程中,都建議開啟這個異常斷點,反正你總是會crash的嘿嘿。
  • Add Symbolic Breakpoint
    Symbolic Breakpoint 為符號斷點。有時候,我們並不清楚程式會在什麼情況下呼叫某一個函式,那我們可以通過符號斷點來獲取呼叫該函式時的程式堆疊。當然,在自己實現的類,我們也可以在該函式實現的地方打上斷點,但如果需要定位其他框架提供的API的呼叫,就只能使用符號斷點啦。

當然,LLDB的breakpoint命令也可以實現上述的功能,因為不常用,所以這裡就簡單列舉一些用法。 breakpoint set -n trigger //在所有類的trigger函式實現中打上斷點

breakpoint set -f ViewController.m -n trigger //在ViewController.m中的trigger方法打上斷點 
    breakpoint set -f ViewController.m -l 50 //在ViewController.m的50行打上斷點 
    breakpoint set -f ViewController.m -n trigger: -c testCondition > 5 //在ViewController.m中的trigger方法打上斷點並新增condition, testCondition大於5時觸發斷點 
    breakpoint set -n trigger -o //單次斷點 
    breakpoint command add -o "frame info" 3 //在設定的三號斷點加入子命令frame info 
    breakpoint list // 列出所有斷點 
    breakpoint delete 3 //刪除3號斷點

2.watchpoint

有時候我們會關心類的某個屬性什麼時候被人修改了,最簡單的方法當然就是在setter的方法打斷點,或者在@property的屬性生命行打上斷點。這樣當物件的setter方法被呼叫時,就會觸發這個斷點。

當然這麼做是有缺點的,對於直接訪問記憶體地址的修改,setter方法的斷點並沒有辦法監控得到,因此我們需要用到watchpoint命令。
watchpoint命令在XCode的GUI中也可以直接使用,當程式暫停時,我們能對當前程式棧中的變數設定watchpoint。值得注意的是,watchpoint是直接設定到該變數所在的記憶體地址上的,所以當這個變數釋放了後,watchpoint仍然是對這個地址的記憶體生效的。

我們也可以在LLDB中直接用watchpoint命令,可以通過選項實現更多效果。

watchpoint set self->testVar     //為該變數地址設定watchpoint
    watchpoint set expression 0x00007fb27b4969e0 //為該記憶體地址設定watchpoint,記憶體地址可從前文提及的`p`命令獲取
    watchpoint command add -o 'frame info' 1  //為watchpoint 1號加上子命令 `frame info`
    watchpoint list //列出所有watchpoint
    watchpoint delete // 刪除所有watchpoint

堆疊

1.thread和bt

bt即是thread backtrace,作用是打印出當前執行緒的堆疊資訊。當程式發生了crash後,我們可以用該命令打印出發生crash的當前的程式堆疊,查詢出發生crash的呼叫路徑。由於比較常用,所以LLDB直接給它一個特殊的bt別名。
thread另一個比較常用的用法是 thread return,除錯的時候,我們希望在當前執行的程式堆疊直接返回一個自己想要的值,可以執行該命令直接返回。
thread return <expr>
在這個斷點中,我們可以執行 thread return NO讓該函式呼叫直接返回NO ,在除錯中輕鬆覆蓋任何函式的返回路徑。

2.frame

frame即是幀,其實就是當前的程式堆疊,我們輸入bt命令,打印出來的其實是當前執行緒的frame。
在除錯中,一般我們比較關心當前堆疊的變數值,我們可以使用frame variable來獲取全部變數值。當然也可以輸入特定變數名,來獲取單獨的變數值,如frame v self-> testVar來獲取testVar的值。

參考地址