Xcode自帶除錯命令集
LLDB的Xcode預設的偵錯程式,它與LLVM編譯器一起,帶給我們更豐富的流程控制和資料檢測的除錯功能。平時用Xcode執行程式,實際走的都是LLDB。熟練使用LLDB,可以讓你debug事半功倍
LLDB基礎知識
LLDB控制檯
Xcode中內嵌了LLDB控制檯,在Xcode中程式碼的下方,我們可以看到LLDB控制檯。
LLDB控制檯平時會輸出一些log資訊。如果我們想輸入命令除錯,必須讓程式進入暫停狀態。讓程式進入暫停狀態的方式主要有2種:
- 斷點或者watchpoint: 在程式碼中設定一個斷點(watchpoint),當程式執行到斷點位置的時候,會進入stop狀態
- 直接暫停,控制檯上方有一個暫停按鈕,上圖紅框已標出,點選即可暫停程式
LLDB語法
在使用LLDB之前,我們來先看看LLDB的語法,瞭解語法可以幫助我們清晰的使用LLDB:
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
一眼看上去可能比較迷茫,給大家解釋一下:
<command>
(命令)和<subcommand>
(子命令):LLDB除錯命令的名稱。命令和子命令按層級結構來排列:一個命令物件為跟隨其的子命令物件建立一個上下文,子命令又為其子命令建立一個上下文,依此類推。<action>
:執行命令的操作<options>
:命令選項<arguement>
:命令的引數[]
:表示命令是可選的,可以有也可以沒有
舉個例子,假設我們給main方法設定一個斷點,我們使用下面的命令:
breakpoint set -n main
這個命令對應到上面的語法就是:
command
:breakpoint
表示斷點命令action
:set
表示設定斷點option
:-n
表示根據方法name設定斷點arguement
:mian
表示方法名為mian
原始(raw)命令
LLDB支援不帶命令選項(options)的原始(raw)命令,原始命令會將命令後面的所有東西當做引數(arguement)傳遞。不過很多原始命令也可以帶命令選項,當你使用命令選項的時候,需要在命令選項後面加--
e.g: 常用的expression
就是raw命令,一般情況下我們使用expression
列印一個東西是這樣的:
(lldb) expression count
(int) $2 = 4
當我們想列印一個物件的時候。需要使用-O
命令選項,我們應該用--
將命令選項和引數區分:
(lldb) expression -O -- self
<ViewController: 0x7f9000f17660>
唯一匹配原則
LLDB的命令遵循唯一匹配原則:假如根據前n個字母已經能唯一匹配到某個命令,則只寫前n個字母等效於寫下完整的命令。
e.g: 前面提到我設定斷點的命令,我們可以使用唯一匹配原則簡寫,下面2條命令等效:
breakpoint set -n main
br s -n main
~/.lldbinit
LLDB有了一個啟動時載入的檔案~/.lldbinit
,每次啟動都會載入。所以一些初始化的事兒,我們可以寫入~/.lldbinit
中,比如給命令定義別名等。但是由於這時候程式還沒有真正執行,也有部分操作無法在裡面玩,比如設定斷點。
LLDB命令
expression
expression命令的作用是執行一個表示式,並將表示式返回的結果輸出。expression的完整語法是這樣的:
expression <cmd-options> -- <expr>
<cmd-options>
:命令選項,一般情況下使用預設的即可,不需要特別標明。--
: 命令選項結束符,表示所有的命令選項已經設定完畢,如果沒有命令選項,--
可以省略<expr>
: 要執行的表示式
說expression
是LLDB裡面最重要的命令都不為過。因為他能實現2個功能。
-
執行某個表示式。
我們在程式碼執行過程中,可以通過執行某個表示式來動態改變程式執行的軌跡。
假如我們在執行過程中,突然想把self.view顏色改成紅色,看看效果。我們不必寫下程式碼,重新run,只需暫停程式,用expression
改變顏色,再重新整理一下介面,就能看到效果// 改變顏色 (lldb) expression -- self.view.backgroundColor = [UIColor redColor] // 重新整理介面 (lldb) expression -- (void)[CATransaction flush]
-
將返回值輸出。
也就是說我們可以通過expression
來列印東西。
假如我們想列印self.view:
(lldb) expression -- self.view
(UIView *) $1 = 0x00007fe322c18a10
p & print & call
一般情況下,我們直接用expression還是用得比較少的,更多時候我們用的是p
、print
、call
。這三個命令其實都是expression
--
的別名(--
表示不再接受命令選項,詳情見前面原始(raw)命令
這一節)
print
: 列印某個東西,可以是變數和表示式p
: 可以看做是print
的簡寫call
: 呼叫某個方法。
表面上看起來他們可能有不一樣的地方,實際都是執行某個表示式(變數也當做表示式),將執行的結果輸出到控制檯上。所以你可以用p
呼叫某個方法,也可以用call
列印東西
e.g: 下面程式碼效果相同:
(lldb) expression -- self.view
(UIView *) $5 = 0x00007fb2a40344a0
(lldb) p self.view
(UIView *) $6 = 0x00007fb2a40344a0
(lldb) print self.view
(UIView *) $7 = 0x00007fb2a40344a0
(lldb) call self.view
(UIView *) $8 = 0x00007fb2a40344a0
(lldb) e self.view
(UIView *) $9 = 0x00007fb2a40344a0
根據唯一匹配原則,如果你沒有自己新增特殊的命令別名。
e
也可以表示expression
的意思。原始命令預設沒有命令選項,所以e
也能帶給你同樣的效果
po
我們知道,OC裡所有的物件都是用指標表示的,所以一般列印的時候,打印出來的是物件的指標,而不是物件本身。如果我們想列印物件。我們需要使用命令選項:-O
。為了更方便的使用,LLDB為expression
-O --
定義了一個別名:po
(lldb) expression -- self.view
(UIView *) $13 = 0x00007fb2a40344a0
(lldb) expression -O -- self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
(lldb) po self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
還有其他很多命令選項,不過我們一般用得比較少,所以我就不具體的一一介紹了,如果想了解,在LLDB控制檯上輸入:
help expression
即可查到expression所有的資訊
thread
thread backtrace
& bt
有時候我們想要了解執行緒堆疊資訊,可以使用thread backtrace
thread backtrace
作用是將執行緒的堆疊打印出來。我們來看看他的語法
thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]
thread backtrace
後面跟的都是命令選項:
-c
:設定列印堆疊的幀數(frame)
-s
:設定從哪個幀(frame)開始列印
-e
:是否顯示額外的回溯
實際上這些命令選項我們一般不需要使用。
e.g: 當發生crash的時候,我們可以使用thread backtrace
檢視堆疊呼叫
(lldb) thread backtrace
* thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame #0: 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11
* frame #1: 0x000000010aa9f75e TLLDB`-[ViewController viewDidLoad](self=0x00007fa270e1f440, _cmd="viewDidLoad") + 174 at ViewController.m:23
frame #2: 0x000000010ba67f98 UIKit`-[UIViewController loadViewIfRequired] + 1198
frame #3: 0x000000010ba682e7 UIKit`-[UIViewController view] + 27
frame #4: 0x000000010b93eab0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
frame #5: 0x000000010b93f199 UIKit`-[UIWindow _setHidden:forced:] + 282
frame #6: 0x000000010b950c2e UIKit`-[UIWindow makeKeyAndVisible] + 42
我們可以看到crash發生在-[ViewController viewDidLoad]
中的第23行,只需檢查這行程式碼是不是幹了什麼非法的事兒就可以了。
LLDB還為backtrace專門定義了一個別名:bt
,他的效果與thread
backtrace
相同,如果你不想寫那麼長一串字母,直接寫下bt
即可:
(lldb) bt
thread return
Debug的時候,也許會因為各種原因,我們不想讓程式碼執行某個方法,或者要直接返回一個想要的值。這時候就該thread
return
上場了。
thread return [<expr>]
thread return
可以接受一個表示式,呼叫命令之後直接從當前的frame返回表示式的值。
e.g: 我們有一個someMethod
方法,預設情況下是返回YES。我們想要讓他返回NO
我們只需在方法的開始位置加一個斷點,當程式中斷的時候,輸入命令即可:
(lldb) thread return NO
效果相當於在斷點位置直接呼叫return NO;
,不會執行斷點後面的程式碼
c & n & s & finish
一般在除錯程式的時候,我們經常用到下面這4個按鈕:
用觸控板的孩子們可能會覺得點選這4個按鈕比較費勁。其實LLDB命令也可以完成上面的操作,而且如果不輸入命令,直接按Enter鍵,LLDB會自動執行上次的命令。按一下Enter就能達到我們想要的效果,有木有頓時感覺逼格滿滿的!!!
我們來看看對應這4個按鈕的LLDB命令:
c
/continue
/thread continue
: 這三個命令效果都等同於上圖中第一個按鈕的。表示程式繼續執行n
/next
/thread step-over
: 這三個命令效果等同於上圖第二個按鈕。表示單步執行s
/step
/thread step-in
: 這三個命令效果等同於上圖第三個按鈕。表示進入某個方法finish
/step-out
: 這兩個命令效果等同於第四個按鈕。表示直接走完當前方法,返回到上層frame
thread其他不常用的命令
thread 相關的還有其他一些不常用的命令,這裡就簡單介紹一下即可,如果需要了解更多,可以使用命令help thread
查閱
thread jump
: 直接讓程式跳到某一行。由於ARC下編譯器實際插入了不少retain,release命令。跳過一些程式碼不執行很可能會造成物件記憶體混亂髮生crash。thread list
: 列出所有的執行緒thread select
: 選擇某個執行緒thread until
: 傳入一個line的引數,讓程式執行到這行的時候暫停thread info
: 輸出當前執行緒的資訊
frame
前面我們提到過很多次frame(幀)。可能有的朋友對frame這個概念還不太瞭解。隨便打個斷點
我們在控制檯上輸入命令bt
,可以打印出來所有的frame。如果仔細觀察,這些frame和左邊紅框裡的堆疊是一致的。平時我們看到的左邊的堆疊就是frame。
frame variable
平時Debug的時候我們經常做的事就是檢視變數的值,通過frame variable
命令,可以打印出當前frame的所有變數
(lldb) frame variable
(ViewController *) self = 0x00007fa158526e60
(SEL) _cmd = "text:"
(BOOL) ret = YES
(int) a = 3
可以看到,他將self
,_cmd
,ret
,a
等本地變數都列印了出來
如果我們要需要列印指定變數,也可以給frame variable
傳入引數:
(lldb) frame variable self->_string
(NSString *) self->_string = nil
不過frame variable
只接受變數作為引數,不接受表示式,也就是說我們無法使用frame
variable self.string
,因為self.string
是呼叫string
的getter
方法。所以一般列印指定變數,我更喜歡用p
或者po
。
其他不常用命令
一般frame variable
列印所有變數用得比較多,frame還有2個不怎麼常用的命令:
frame info
: 檢視當前frame的資訊
(lldb) frame info
frame #0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at ViewController.m:38
frame select
: 選擇某個frame
(lldb) frame select 1
frame #1: 0x0000000101bf872e TLLDB`-[ViewController viewDidLoad](self=0x00007fa158526e60, _cmd="viewDidLoad") + 78 at ViewController.m:23
20
21 - (void)viewDidLoad {
22 [super viewDidLoad];
-> 23 [self text:YES];
24 NSLog(@"1");
25 NSLog(@"2");
26 NSLog(@"3");
當我們選擇frame 1的時候,他會把frame1的資訊和程式碼打印出來。不過一般我都是直接在Xcode左邊點選某個frame,這樣更方便
breakpoint
除錯過程中,我們用得最多的可能就是斷點了。LLDB中的斷點命令也非常強大
breakpoint set
breakpoint set
命令用於設定斷點,LLDB提供了很多種設定斷點的方式:
使用-n
根據方法名設定斷點:
e.g: 我們想給所有類中的viewWillAppear:
設定一個斷點:
(lldb) breakpoint set -n viewWillAppear:
Breakpoint 13: 33 locations.
使用-f
指定檔案
e.g: 我們只需要給ViewController.m
檔案中的viewDidLoad
設定斷點:
(lldb) breakpoint set -f ViewContr