LLDB的Xcode默認(rèn)的調(diào)試器,它與LLVM編譯器一起,帶給我們更豐富的流程控制和數(shù)據(jù)檢測(cè)的調(diào)試功能。平時(shí)用Xcode運(yùn)行程序,實(shí)際走的都是LLDB。熟練使用LLDB,可以讓你debug事半功倍
LLDB基礎(chǔ)知識(shí)
LLDB控制臺(tái)
Xcode中內(nèi)嵌了LLDB控制臺(tái),在Xcode中代碼的下方,我們可以看到LLDB控制臺(tái)。
LLDB控制臺(tái)平時(shí)會(huì)輸出一些log信息。如果我們想輸入命令調(diào)試,必須讓程序進(jìn)入暫停狀態(tài)。讓程序進(jìn)入暫停狀態(tài)的方式主要有2種:
斷點(diǎn)或者watchpoint: 在代碼中設(shè)置一個(gè)斷點(diǎn)(watchpoint),當(dāng)程序運(yùn)行到斷點(diǎn)位置的時(shí)候,會(huì)進(jìn)入stop狀態(tài)
直接暫停,控制臺(tái)上方有一個(gè)暫停按鈕,上圖紅框已標(biāo)出,點(diǎn)擊即可暫停程序
LLDB語(yǔ)法
在使用LLDB之前,我們來(lái)先看看LLDB的語(yǔ)法,了解語(yǔ)法可以幫助我們清晰的使用LLDB:
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
一眼看上去可能比較迷茫,給大家解釋一下:
-
<command>
(命令)和<subcommand>
(子命令):LLDB調(diào)試命令的名稱(chēng)。命令和子命令按層級(jí)結(jié)構(gòu)來(lái)排列:一個(gè)命令對(duì)象為跟隨其的子命令對(duì)象創(chuàng)建一個(gè)上下文,子命令又為其子命令創(chuàng)建一個(gè)上下文,依此類(lèi)推。 -
<action>
:執(zhí)行命令的操作 -
<options>
:命令選項(xiàng) -
<arguement>
:命令的參數(shù) -
[]
:表示命令是可選的,可以有也可以沒(méi)有
舉個(gè)例子,假設(shè)我們給main方法設(shè)置一個(gè)斷點(diǎn),我們使用下面的命令:
breakpoint set -n main
這個(gè)命令對(duì)應(yīng)到上面的語(yǔ)法就是:
command
:breakpoint
表示斷點(diǎn)命令
action
:set
表示設(shè)置斷點(diǎn)
option
:-n
表示根據(jù)方法name設(shè)置斷點(diǎn)
arguement
:mian
表示方法名為mian
原始(raw)命令
LLDB支持不帶命令選項(xiàng)(options)的原始(raw)命令,原始命令會(huì)將命令后面的所有東西當(dāng)做參數(shù)(arguement)傳遞。不過(guò)很多原始命令也可以帶命令選項(xiàng),當(dāng)你使用命令選項(xiàng)的時(shí)候,需要在命令選項(xiàng)后面加--區(qū)分命令選項(xiàng)和參數(shù)。
e.g: 常用的expression
就是raw命令,一般情況下我們使用expression
打印一個(gè)東西是這樣的:
(lldb) expression count
(int) $2 = 4
當(dāng)我們想打印一個(gè)對(duì)象的時(shí)候。需要使用-O
命令選項(xiàng),我們應(yīng)該用--
將命令選項(xiàng)和參數(shù)區(qū)分:
(lldb) expression -O -- self
<ViewController: 0x7f9000f17660>
唯一匹配原則
LLDB的命令遵循唯一匹配原則:假如根據(jù)前n個(gè)字母已經(jīng)能唯一匹配到某個(gè)命令,則只寫(xiě)前n個(gè)字母等效于寫(xiě)下完整的命令。e.g: 前面提到我設(shè)置斷點(diǎn)的命令,我們可以使用唯一匹配原則簡(jiǎn)寫(xiě),下面2條命令等效:
breakpoint set -n main
br s -n main
~/.lldbinit
LLDB有了一個(gè)啟動(dòng)時(shí)加載的文件~/.lldbinit
,每次啟動(dòng)都會(huì)加載。所以一些初始化的事兒,我們可以寫(xiě)入~/.lldbinit
中,比如給命令定義別名等。但是由于這時(shí)候程序還沒(méi)有真正運(yùn)行,也有部分操作無(wú)法在里面玩,比如設(shè)置斷點(diǎn)。
LLDB命令
expression
expression命令的作用是執(zhí)行一個(gè)表達(dá)式,并將表達(dá)式返回的結(jié)果輸出。expression的完整語(yǔ)法是這樣的:
expression <cmd-options> -- <expr>
-
<cmd-options>
:命令選項(xiàng),一般情況下使用默認(rèn)的即可,不需要特別標(biāo)明。 -
--
: 命令選項(xiàng)結(jié)束符,表示所有的命令選項(xiàng)已經(jīng)設(shè)置完畢,如果沒(méi)有命令選項(xiàng),--
可以省略 -
<expr>
: 要執(zhí)行的表達(dá)式
說(shuō)expression
是LLDB里面最重要的命令都不為過(guò)。因?yàn)樗軐?shí)現(xiàn)2個(gè)功能。
- 執(zhí)行某個(gè)表達(dá)式。
我們?cè)诖a運(yùn)行過(guò)程中,可以通過(guò)執(zhí)行某個(gè)表達(dá)式來(lái)動(dòng)態(tài)改
變程序運(yùn)行的軌跡。假如我們?cè)谶\(yùn)行過(guò)程中,突然想把self.view顏色改成紅色,
看看效果。我們不必寫(xiě)下代碼,重新run,只需暫停程序,用expression
改變顏色,再刷新一下界面,就能看到效果
// 改變顏色
(lldb) expression -- self.view.backgroundColor = [UIColor redColor]
// 刷新界面
(lldb) expression -- (void)[CATransaction flush]
- 將返回值輸出。
也就是說(shuō)我們可以通過(guò)expression來(lái)打印東西。
假如我們想打印self.view:
(lldb) expression -- self.view
(UIView *) $1 = 0x00007fe322c18a10
p & print & call
一般情況下,我們直接用expression還是用得比較少的,更多時(shí)候我們用的是p
、print
、call
。這三個(gè)命令其實(shí)都是expression --
的別名(--
表示不再接受令選項(xiàng))
1.print
: 打印某個(gè)東西,可以是變量和表達(dá)式
2.p
: 可以看做是print的簡(jiǎn)寫(xiě)
3.call
: 調(diào)用某個(gè)方法。
表面上看起來(lái)他們可能有不一樣的地方,實(shí)際都是執(zhí)行某個(gè)表達(dá)式(變量也當(dāng)做表達(dá)式),將執(zhí)行的結(jié)果輸出到控制臺(tái)上。所以你可以用p
調(diào)用某個(gè)方法,也可以用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
根據(jù)唯一匹配原則,如果你沒(méi)有自己添加特殊的命令別名。e也可以表示
expression
的意思。原始命令默認(rèn)沒(méi)有命令選項(xiàng),所以e
也能帶給你同樣的效果
po
我們知道,OC里所有的對(duì)象都是用指針表示的,所以一般打印的時(shí)候,打印出來(lái)的是對(duì)象的指針,而不是對(duì)象本身。如果我們想打印對(duì)象。我們需要使用命令選項(xiàng):-O
。為了更方便的使用,LLDB為expression -O --
定義了一個(gè)別名: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>>
還有其他很多命令選項(xiàng),不過(guò)我們一般用得比較少,所以我就不具體的一一介紹了,如果想了解,在LLDB控制臺(tái)上輸入:help expression
即可查到expression
所有的信息
thread
thread backtrace
& bt
有時(shí)候我們想要了解線程堆棧信息,可以使用thread backtrace
thread backtrace
作用是將線程的堆棧打印出來(lái)。我們來(lái)看看他的語(yǔ)法
thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]
thread backtrace
后面跟的都是命令選項(xiàng):
-c
:設(shè)置打印堆棧的幀數(shù)(frame)
-s
:設(shè)置從哪個(gè)幀(frame)開(kāi)始打印
-e
:是否顯示額外的回溯實(shí)際上這些命令選項(xiàng)我們一般不需要使用。
e.g: 當(dāng)發(fā)生crash的時(shí)候,我們可以使用thread backtrace
查看堆棧調(diào)用
(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發(fā)生在-[ViewController viewDidLoad]
中的第23行,只需檢查這行代碼是不是干了什么非法的事兒就可以了。
LLDB還為backtrace
專(zhuān)門(mén)定義了一個(gè)別名:bt
,他的效果與thread backtrace
相同,如果你不想寫(xiě)那么長(zhǎng)一串字母,直接寫(xiě)下bt
即可:
(lldb) bt
thread return
Debug的時(shí)候,也許會(huì)因?yàn)楦鞣N原因,我們不想讓代碼執(zhí)行某個(gè)方法,或者要直接返回一個(gè)想要的值。這時(shí)候就該thread return
上場(chǎng)了。
thread return [<expr>]
thread return
可以接受一個(gè)表達(dá)式,調(diào)用命令之后直接從當(dāng)前的frame返回表達(dá)式的值。
e.g: 我們有一個(gè)someMethod
方法,默認(rèn)情況下是返回YES。我們想要讓他返回NO
我們只需在方法的開(kāi)始位置加一個(gè)斷點(diǎn),當(dāng)程序中斷的時(shí)候,輸入命令即可:
(lldb) thread return NO
效果相當(dāng)于在斷點(diǎn)位置直接調(diào)用return NO
;不會(huì)執(zhí)行斷點(diǎn)后面的代碼
c & n & s & finish
一般在調(diào)試程序的時(shí)候,我們經(jīng)常用到下面這4個(gè)按鈕:
用觸摸板的孩子們可能會(huì)覺(jué)得點(diǎn)擊這4個(gè)按鈕比較費(fèi)勁。其實(shí)LLDB命令也可以完成上面的操作,而且如果不輸入命令,直接按Enter鍵,LLDB會(huì)自動(dòng)執(zhí)行上次的命令。按一下Enter就能達(dá)到我們想要的效果,有木有頓時(shí)感覺(jué)逼格滿滿的!!!
我們來(lái)看看對(duì)應(yīng)這4個(gè)按鈕的LLDB命令:
1.c/continue/thread continue
: 這三個(gè)命令效果都等同于上圖中第一個(gè)按鈕的。表示程序繼續(xù)運(yùn)行
2.n/next/thread step-over
: 這三個(gè)命令效果等同于上圖第二個(gè)按鈕。表示單步運(yùn)行
3.s/step/thread step-in
: 這三個(gè)命令效果等同于上圖第三個(gè)按鈕。表示進(jìn)入某個(gè)方法
4.finish/step-out
: 這兩個(gè)命令效果等同于第四個(gè)按鈕。表示直接走完當(dāng)前方法,返回到上層frame
thread其他不常用的命令
thread 相關(guān)的還有其他一些不常用的命令,這里就簡(jiǎn)單介紹一下即可,如果需要了解更多,可以使用命令help thread
查閱
1.thread jump
: 直接讓程序跳到某一行。由于ARC下編譯器實(shí)際插入了不少retain,release命令。跳過(guò)一些代碼不執(zhí)行很可能會(huì)造成對(duì)象內(nèi)存混亂發(fā)生crash。
2.thread list
: 列出所有的線程
3.thread select
: 選擇某個(gè)線程
4.thread until
: 傳入一個(gè)line的參數(shù),讓程序執(zhí)行到這行的時(shí)候暫停
5.thread info
: 輸出當(dāng)前線程的信息
frame
前面我們提到過(guò)很多次frame(幀)。可能有的朋友對(duì)frame這個(gè)概念還不太了解。隨便打個(gè)斷點(diǎn)
我們?cè)诳刂婆_(tái)上輸入命令bt
,可以打印出來(lái)所有的frame。如果仔細(xì)觀察,這些frame和左邊紅框里的堆棧是一致的。平時(shí)我們看到的左邊的堆棧就是frame。
frame variable
平時(shí)Debug的時(shí)候我們經(jīng)常做的事就是查看變量的值,通過(guò)frame variable
命令,可以打印出當(dāng)前frame的所有變量
(lldb) frame variable(ViewController *) self = 0x00007fa158526e60(SEL) _cmd = "text:"(BOOL) ret = YES(int) a = 3
可以看到,他將self
,_cmd
,ret
,a
等本地變量都打印了出來(lái),如果我們要需要打印指定變量,也可以給frame variable
傳入?yún)?shù):
(lldb) frame variable self->_string(NSString *) self->_string = nil
不過(guò)frame variable
只接受變量作為參數(shù),不接受表達(dá)式,也就是說(shuō)我們無(wú)法使用frame variable self.string
,因?yàn)閟elf.string
是調(diào)用string
的getter
方法。所以一般打印指定變量,我更喜歡用p
或者po
。
其他不常用命令
一般frame variable
打印所有變量用得比較多
frame還有2個(gè)不怎么常用的命令:
frame info
: 查看當(dāng)前frame的信息
(lldb) frame infoframe #0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at ViewController.m:38
frame select
: 選擇某個(gè)frame
(lldb) frame select 1frame #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");
當(dāng)我們選擇frame 1的時(shí)候,他會(huì)把frame1的信息和代碼打印出來(lái)。不過(guò)一般我都是直接在Xcode左邊點(diǎn)擊某個(gè)frame,這樣更方便