相信大家肯定都有過為了調(diào)試而添加打印變量厉斟,或者使用直接常量代替函數(shù)調(diào)用結(jié)果,或者更改判斷條件以進入某特定分支的調(diào)試經(jīng)歷,但每次更改代碼都需要重新編譯形葬,重新來過一遍却汉,但其實本可以不用這樣,因為我們有調(diào)試器荷并,而且除了監(jiān)視變量的值合砂,還有很多它可以做的。
LLDB
LLDB是一個開源的以REPL為特性源织,并可以配置C++和python插件的調(diào)試器翩伪。它集成在xcode中,并在窗口底部的控制臺中運行谈息。調(diào)試器可以暫停程序執(zhí)行缘屹,觀察變量,執(zhí)行自定義指令侠仇,并掌控程序的執(zhí)行轻姿。如果對GDB比較熟悉的話,GDB-TO-LLDB?這個份指引應(yīng)該對你了解LLDB的指令有極大助益逻炊,如果安裝了Chisel這個LLDB插件的話互亮,調(diào)試會更有趣。
基礎(chǔ)操作
程序在斷點處暫停的時候余素,console會打開供輸入命令:
help
最簡單的命令即是help豹休,它會列出所有命令,如果忘記了help本身桨吊,help help試試
print?
用來打印值威根,由于 LLDB的前綴匹配,所以也可以使用prin,pri,但不可以用pr,因為還有一個命令是process视乐,但可以用p代表print洛搀。
注意到結(jié)果里面會有$n的字樣,帶$前綴的標識屬于LLDB命名空間佑淀,我們可以利用這個特性為自己服務(wù)
expression
修改變量值
注:有一個需要注意的是留美,如果使用中文字符串常量的時候,由于LLDB解析器的bug渣聚,會報錯An Objective-C constant string's string initializer is not an array独榴,需要使用[NSString stringWithUTF8String:]
list命令
list 行號 ? ? ? 顯示行號開始的數(shù)行代碼(默認10行)
list 函數(shù)名 ? ?顯示函數(shù)名為中心的前后十行代碼
list不帶參數(shù),接著上一次list命令
print 命令
如果執(zhí)行 p count = 18,會發(fā)現(xiàn)除了打印18之后奕枝,count的值也會變成18.它與expression count = 18執(zhí)行結(jié)果相同棺榔。不同的地方在于print 命令沒有參數(shù)。
想想 e -h +17 這行命令隘道,如果將-h理解為flag的話症歇, +17看起來并不像輸入郎笆。如果理解為計算17與h的差值,那這個連字符看起來很讓人困惑忘晤。
幸運的是宛蚓,使用--來分隔flag與其后的輸入,實際上e -- 的縮寫是print设塔。
打印對象
如果使用print objects,則輸入看起來十分冗長:(NSString *) $7 = 0x0000000104da4040 @"red balloons"凄吏,如果打印更復(fù)雜的結(jié)構(gòu) p @[ @"foo", @"bar" ],輸出可能像這樣(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"闰蛔,而實際上我們想看對象的description痕钢,所以需要將對象按對象輸出,使用-O 選項序六。
同樣幸運的是任连,e -O --的別名是po (print object)
打印變量
可以在print命令中指定很多種格式,其形式像這樣print/<fmt>,或者p/<fmt>例诀,默認格式是這樣随抠,p 16 > 16,十六進制形式輸出 p/x 16 > 0x10, 二進制 (t代表two) p/t 16 > 0b10000
還可以用p/c輸出字符繁涂,p/s輸出c字符串拱她,這里是完整的輸出格式
變量
在lldb中可以使用變量以減少過多的typing,但在LLDB中定義的變量必須以$開頭爆土,比如e int $i = 1,e NSArray* $a = @[@"Saturday",@"Sunday"], p [$a count], po [[$a objectAtIndex:0] uppercaseString]
而如果輸入這條命令p [[$a objectAtIndex:$i] characterAtIndex:0],得到的結(jié)果是
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression
因為LLDB無法識別涉及到的類型椭懊,這種情況有時候會發(fā)生,可以這樣處理
p (char)[[$a objectAtIndex:$i] characterAtIndex:0] ?> 'M'
或者p/d (char)[[$a objectAtIndex:$i] characterAtIndex:0] > 77
執(zhí)行流控制
命中斷點時步势,調(diào)試條上的4個按鈕可供控制程序的執(zhí)行流
其意義從左到右分別是繼續(xù),越過背犯,進入函數(shù)體坏瘩,退出函數(shù)體
繼續(xù)按鈕的作用同LLDB的process continue,簡寫為continue,或者c
越過按鈕的作用相當于執(zhí)行完當前行的指令漠魏,就算當前當為函數(shù)調(diào)用倔矾,也不進入函數(shù)體,對應(yīng)LLDB中的thread step-over,next 或者n
進入函數(shù)體按鈕的作用相當于LLDB中的 thread step-in,step和s柱锹,而如果當前行非函數(shù)調(diào)用哪自,則其與thread step-over 表現(xiàn)是相同的
退出函數(shù)體的作用在于能夠一直執(zhí)行到當前函數(shù)return之后再中斷到調(diào)試器,即將當前棧幀彈出之后再停止禁熏。對應(yīng)LLDB中的命令finish
frame info
輸出當前代碼行及其所在在源代碼文件
線程return
thread return是控制執(zhí)行流的另一利器壤巷,其可攜帶選項參數(shù),其中將參數(shù)加載進返回寄存器瞧毙,并立即執(zhí)行return命令胧华,并跳出當前棧幀寄症,而這也意味著當前函數(shù)中剩余的語句不會被執(zhí)行。這樣做導(dǎo)致ARC的引用計數(shù)和追蹤方面的問題或者在函數(shù)尾所做的清理等被跳過矩动,但如果在進入函數(shù)體之后馬上執(zhí)行有巧,則其在假裝函數(shù)已經(jīng)執(zhí)行方面的作用還是很大的。
斷點篇
LLDB列舉斷點: breakpoint list/br li
enable/disable 斷點: breakpoint enable <breakpointID>/breakpoint disable <breakpointID>
設(shè)置斷點:(在Xcode中可以在編輯區(qū)的代碼行首點擊添加斷點悲没,以及鼠標拖拽斷點到代碼行首區(qū)釋放以移除斷點)
breakpoint set:? 比如breakpoint set -f main.m -l 16
由于b是_regexp-break的簡寫篮迎,所以breakpoint的縮寫為br
但其實使用b設(shè)置斷點,LLDB在通常情況下也是可以識別的示姿,比如b main.m:17同樣可以設(shè)置斷點成功
其實也可以使用符號(C函數(shù)來設(shè)置斷點,不用指定行號),比如
(lldb) b isEven
(lldb) br s -F isEven
這樣可以使得調(diào)用此函數(shù)時會在其入口暫停柑潦,也可以使用oc 方法設(shè)置斷點
(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
(lldb) b -[NSArray objectAtIndex:]
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
(lldb) b +[NSSet setWithObject:]
如果想創(chuàng)建一個符號斷點,可以在斷點導(dǎo)航頁中點擊“+”
同時在右鍵每個現(xiàn)有斷點時都會出現(xiàn)包含edit breakpoint項的菜單峻凫,點擊之后
Add Action的動作在如下詳細介紹
Breakpoint Actions
這個功能很有用渗鬼,可以在斷點發(fā)生之后,馬上執(zhí)行你所定義的動作之后再將控制權(quán)交給你荧琼,即lldb命令行譬胎。action支持多行Debugger command,shell command,log message,使用lldb而非UI操作的方法為
Continuing after Evaluation
在編輯斷點的UI中命锄,options項Automatically continue after evaluation actions勾選可以使得在斷點處執(zhí)行完預(yù)設(shè)action之后馬上恢復(fù)運行堰乔,就像沒斷點一樣。
Full Execution in the Debugger
還有一個特性是可以在debugger中運行任何C/Objective-C/C++/Swift命令脐恩,一點不足之外是不能創(chuàng)建新的函數(shù)镐侯,即不能創(chuàng)建新類,block,函數(shù)驶冒,帶虛方法的C++類等苟翻,除此之外都可以。
比如可以分配數(shù)字節(jié)的空間
不過要注意骗污,分配空間的時候崇猫,其作用域是與當前棧幀中當前代碼行所在的作用域相同的,所以要盡量避免因作用域問題引起的執(zhí)行異常
也可以使用x命令查看新數(shù)組的4個字節(jié)
(lldb) x/4c $str
0x7fd04a900040: monk
也可以查看數(shù)組第3個字節(jié)開始的內(nèi)容(x命令需要反引號需忿,需要地址作為參數(shù)):
(lldb) x/1w `$str + 3`
0x7fd04a900043: keys
但所有這些操作結(jié)束之后诅炉,確保釋放這些內(nèi)存,以免引起內(nèi)存泄漏(在調(diào)試器中):
(lldb) e (void)free($str)
根據(jù)以上內(nèi)容我們可以做的
在調(diào)試條中的暫停按鈕可以暫停當前app,實際是其是執(zhí)行了process interrupt屋厘,因為調(diào)試器其實一直都在執(zhí)行場景之中涕烧。雖然此時的中斷可能并沒有暫停在你熟悉的代碼上下文中,但其實可以試試這個:
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
更新UI
基于上述輸出汗洒,可以從UI層級中選擇一個來操作议纯,假定其地址為0xadd2e55
(lldb) e id $myView = (id)0xadd2e55
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
走到恢復(fù)app的執(zhí)行才會看到UI的變化,因為需要將這個信息通知給渲染Server顯示才會更新仲翎。
render server實際是另一個進程backboardd痹扇,但在我們在調(diào)試當前進程的時候铛漓,backboardd是沒有暫停的,所以其實我們可以執(zhí)行(lldb) e (void)[CATransaction flush]鲫构,這樣就可以在不恢復(fù)執(zhí)行的情況下看到UI更新浓恶。chisel提供了caflush來完成這個更新的功能(chisel還有更多方便的功能)
Pushing a View Controller
假設(shè)當前app的root vc為UINavigationController,則
(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController](lldb) e id $vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
(lldb) e (void)[$vc setTitle:@"Yay!"]
(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
即可以添加一個子view controller,然后執(zhí)行(lldb) caflush // e (void)[CATransaction flush]结笨,就可以看見一個view controller被push進當前View Controller層級
筆者在使用presentViewController測試的時候包晰,執(zhí)行flush之后未見到UI有更新,然后執(zhí)行continue才看到vc被present進來炕吸,xcode 6.4,iOS 8.3伐憾,真機和模擬器均是這樣
Finding the Target of a Button
如果想知道$myButton相應(yīng)的監(jiān)聽action有哪些,可以這樣
(lldb) po [$myButton allTargets]{()}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)
這個時候赫模,對_handleTap就想怎樣就怎樣設(shè)置斷點
觀察實例變量的更改
假定UIView的_layer被覆寫树肃,由于不涉及到方法的調(diào)用,無法設(shè)置斷點瀑罗。于是我們可以設(shè)置對某地址的寫入胸嘴,先找下_layer 變量在對象中的所在
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8
于是我們知道$myView+8即是我們關(guān)心的地址,于是
(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
new value: 0x0000000000000000
在Chisel中斩祭,上述功能被精簡為命令wivar $myView _layer
未被重載的方法上的符號化斷點
假定想對-[MyViewController viewDidAppear:]的調(diào)用時機做監(jiān)聽劣像,但vc本身未重載此方法,如果這樣設(shè)置斷點
(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:? Unable to resolve breakpoint to any actual locations.
但由于未重載此方法摧玫,所以不會有viewDidAppear的符號耳奕,故斷點不會命中。這種情況下需要設(shè)置條件 [self isKindOfClass:[MyViewController class]]诬像,并將此條件置于UIViewController之上屋群。但由于我們并未擁有UIViewController viewDidAppear的實現(xiàn),其為apple所編寫颅停,故沒有符號 谓晌;且在此方法之內(nèi)并沒有self可用。由于如果在符號化斷點中想用self, 那首先要知道self的位置(可能在寄存器中也可能在棧上癞揉,x86中可以在$esp+4中找到,但最少有4種硬件架構(gòu)啊溺欧,親)喊熟。難以想象了解每種硬件架構(gòu)的指令集和調(diào)用規(guī)則,然后再編寫在正確的父類上使用正確的條件設(shè)置斷點的命令姐刁。幸運的是芥牌,使用Chisel可以很輕松地做到:
(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c
這篇文章的作者為了推廣Chisel也是夠拼的⊙﹏⊙
LLDB and Python
LLDB擁有全面而且是內(nèi)置的Python支持,如果在LLDB中輸入script聂使,會打開Python REPL(Read-Eval-Print Loop),相當于python命令行壁拉。當然也可以使用script command執(zhí)行python命令谬俄。
(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")
接下來的事情就多了,可以使用py腳本文件弃理,比如~/myCommands.py
def caflushCommand(debugger, command, result, internal_dict):
debugger.HandleCommand("e (void)[CATransaction flush]")
然后在lldb中執(zhí)行
(lldb) script import ~/myCommands.py
而且溃论,也可以將上述導(dǎo)入放入/.lldbinit以在每次LLDB啟動時都執(zhí)行,Chisel所做的也都是拼接字符串并交給LLDB執(zhí)行而已痘昌。
錯誤處理
1 退出lldb repl的方法钥勋,使用 : 命令
2 一個需要注意的問題是在lldb中創(chuàng)建對象的時候,從init方法返回時辆苔,可能會報錯 cast the message send to the methods return type算灸,這時候不要慌,將對象顯式轉(zhuǎn)換為相應(yīng)的對象類型驻啤,比如 (CGRect)[[UIView alloc] init],另外也需要 (CGRect)[view frame]
因為lldb并不支持.語法方法調(diào)用菲驴,同時還需要顯式轉(zhuǎn)換以及必要時候的括號將整個對象包裹起來,以告訴lldb骑冗,我們的輸入表示的絕對是一個對象
3 如果在lldb中輸入expr -- content.text = (NSString*)[NSString stringWithFormat:@"e"]