你是否曾經(jīng)苦惱于理解你的代碼喘先,而去嘗試打印一個(gè)變量的值庸论?
NSLog(@"%@", whatIsInsideThisThing);
或者跳過一個(gè)函數(shù)調(diào)用來簡(jiǎn)化程序的行為欺劳?
NSNumber *n = @7;//實(shí)際應(yīng)該調(diào)用這個(gè)函數(shù):Foo();
或者短路一個(gè)邏輯檢查愈犹?
if(1||theBooleanAtStake) { ... }
或者偽造一個(gè)函數(shù)實(shí)現(xiàn)街图?
intcalculateTheTrickyValue {return9;/*
先這么著
...
}
并且每次必須重新編譯,從頭開始金吗?
構(gòu)建軟件是復(fù)雜的十兢,并且 Bug 總會(huì)出現(xiàn)趣竣。一個(gè)常見的修復(fù)周期就是修改代碼,編譯旱物,重新運(yùn)行遥缕,并且祈禱出現(xiàn)最好的結(jié)果。
但是不一定要這么做宵呛。你可以使用調(diào)試器通砍。而且即使你已經(jīng)知道如何使用調(diào)試器檢查變量,它可以做的還有很多烤蜕。
這篇文章將試圖挑戰(zhàn)你對(duì)調(diào)試的認(rèn)知,并詳細(xì)地解釋一些你可能還不了解的基本原理迹冤,然后展示一系列有趣的例子》碛現(xiàn)在就讓我們開始與調(diào)試器共舞一曲華爾茲,看看最后能達(dá)到怎樣的高度泡徙。
LLDB
LLDB是一個(gè)有著 REPL 的特性和 C++ ,Python 插件的開源調(diào)試器橱鹏。LLDB 綁定在 Xcode 內(nèi)部,存在于主窗口底部的控制臺(tái)中堪藐。調(diào)試器允許你在程序運(yùn)行的特定時(shí)暫停它莉兰,你可以查看變量的值,執(zhí)行自定的指令礁竞,并且按照你所認(rèn)為合適的步驟來操作程序的進(jìn)展糖荒。(這里有一個(gè)關(guān)于調(diào)試器如何工作的總體的解釋。)
你以前有可能已經(jīng)使用過調(diào)試器模捂,即使只是在 Xcode 的界面上加一些斷點(diǎn)捶朵。但是通過一些小的技巧,你就可以做一些非晨衲校酷的事情综看。GDB to LLDB參考是一個(gè)非常好的調(diào)試器可用命令的總覽。你也可以安裝Chisel岖食,它是一個(gè)開源的 LLDB 插件合輯红碑,這會(huì)使調(diào)試變得更加有趣。
與此同時(shí)泡垃,讓我們以在調(diào)試器中打印變量來開始我們的旅程吧析珊。
基礎(chǔ)
這里有一個(gè)簡(jiǎn)單的小程序,它會(huì)打印一個(gè)字符串兔毙。注意斷點(diǎn)已經(jīng)被加在第 8 行唾琼。斷點(diǎn)可以通過點(diǎn)擊 Xcode 的源碼窗口的側(cè)邊槽進(jìn)行創(chuàng)建。
程序會(huì)在這一行停止運(yùn)行澎剥,并且控制臺(tái)會(huì)被打開锡溯,允許我們和調(diào)試器交互赶舆。那我們應(yīng)該打些什么呢?
help
最簡(jiǎn)單命令是help祭饭,它會(huì)列舉出所有的命令芜茵。如果你忘記了一個(gè)命令是做什么的,或者想知道更多的話倡蝙,你可以通過help 來了解更多細(xì)節(jié)九串,例如help print或者h(yuǎn)elp thread。如果你甚至忘記了help命令是做什么的寺鸥,你可以試試help help猪钮。不過你如果知道這么做,那就說明你大概還沒有忘光這個(gè)命令胆建。??
打印值很簡(jiǎn)單烤低;只要試試print命令:
LLDB 實(shí)際上會(huì)作前綴匹配。所以你也可以使用prin笆载,pri扑馁,或者p。但你不能使用pr凉驻,因?yàn)?LLDB 不能消除和process的歧義 (幸運(yùn)的是p并沒有歧義)腻要。
你可能還注意到了,結(jié)果中有個(gè)$0涝登。實(shí)際上你可以使用它來指向這個(gè)結(jié)果雄家。試試print $0 + 7,你會(huì)看到106缀拭。任何以美元符開頭的東西都是存在于 LLDB 的命名空間的咳短,它們是為了幫助你進(jìn)行調(diào)試而存在的。
expression
如果想改變一個(gè)值怎么辦蛛淋?你或許會(huì)猜modify咙好。其實(shí)這時(shí)候我們要用到的是expression這個(gè)方便的命令。
這不僅會(huì)改變調(diào)試器中的值褐荷,實(shí)際上它改變了程序中的值勾效。這時(shí)候繼續(xù)執(zhí)行程序,將會(huì)打印42 red balloons叛甫。神奇吧层宫。
注意,從現(xiàn)在開始其监,我們將會(huì)偷懶分別以p和e來代替print和expression萌腿。
什么是print命令
考慮一個(gè)有意思的表達(dá)式:p count = 18。如果我們運(yùn)行這條命令抖苦,然后打印count的內(nèi)容毁菱。我們將看到它的結(jié)果與expression count = 18一樣米死。
和expression不同的是,print命令不需要參數(shù)贮庞。比如e -h +17中峦筒,你很難區(qū)分到底是以-h為標(biāo)識(shí),僅僅執(zhí)行+17呢窗慎,還是要計(jì)算17和h的差值物喷。連字符號(hào)確實(shí)很讓人困惑,你或許得不到自己想要的結(jié)果遮斥。
幸運(yùn)的是峦失,解決方案很簡(jiǎn)單。用--來表征標(biāo)識(shí)的結(jié)束术吗,以及輸入的開始宠进。如果想要-h作為標(biāo)識(shí),就用e -h -- +17藐翎,如果想計(jì)算它們的差值,就使用e -- -h +17实幕。因?yàn)橐话銇碚f不使用標(biāo)識(shí)的情況比較多吝镣,所以e --就有了一個(gè)簡(jiǎn)寫的方式,那就是print昆庇。
輸入help print末贾,然后向下滾動(dòng),你會(huì)發(fā)現(xiàn):
'print'is an abbreviationfor'expression --'.? (print是`expression --`的縮寫)
打印對(duì)象
嘗試輸入
p objects
輸出會(huì)有點(diǎn)啰嗦
(NSString *) $7 =0x0000000104da4040@"red balloons"
如果我們嘗試打印結(jié)構(gòu)更復(fù)雜的對(duì)象整吆,結(jié)果甚至?xí)?/p>
(lldb) p @[ @"foo", @"bar"](NSArray *) $8 =0x00007fdb9b71b3e0@"2 objects"
實(shí)際上拱撵,我們想看的是對(duì)象的description方法的結(jié)果。我么需要使用-O(字母 O表蝙,而不是數(shù)字 0) 標(biāo)志告訴expression命令以對(duì)象(Object) 的方式來打印結(jié)果拴测。
(lldb) e -O -- $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)
幸運(yùn)的是,e -o --有也有個(gè)別名府蛇,那就是po(printobject 的縮寫)集索,我們可以使用它來進(jìn)行簡(jiǎn)化:
(lldb) po $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)(lldb) po @"lunar"lunar(lldb) p @"lunar"(NSString *) $13 =0x00007fdb9d0003b0@"lunar"
打印變量
可以給print指定不同的打印格式。它們都是以print/或者簡(jiǎn)化的p/格式書寫汇跨。下面是一些例子:
默認(rèn)的格式
(lldb) p 16
16
十六進(jìn)制:
(lldb) p/x 16
0x10
二進(jìn)制 (t代表two):
(lldb) p/t160b00000000000000000000000000010000(lldb) p/t (char)160b00010000
你也可以使用p/c打印字符务荆,或者p/s打印以空終止的字符串 (譯者注:以 '\0' 結(jié)尾的字符串)。
這里是格式的完整清單穷遂。
變量
現(xiàn)在你已經(jīng)可以打印對(duì)象和簡(jiǎn)單類型函匕,并且知道如何使用expression命令在調(diào)試器中修改它們了。現(xiàn)在讓我們使用一些變量來減少輸入量蚪黑。就像你可以在 C 語(yǔ)言中用int a = 0來聲明一個(gè)變量一樣盅惜,你也可以在 LLDB 中做同樣的事情中剩。不過為了能使用聲明的變量,變量必須以美元符開頭酷窥。
(lldb) e int $a =2(lldb) p $a *1938(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday"](lldb) p [$array count]2(lldb) po [[$arrayobjectAtIndex:0] uppercaseString]SATURDAY(lldb) p [[$arrayobjectAtIndex:$a]characterAtIndex:0]error:no known method'-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression
悲劇了咽安,LLDB 無法確定涉及的類型 (譯者注:返回的類型)。這種事情常常發(fā)生蓬推,給個(gè)說明就好了:
(lldb) p (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]'M'(lldb) p/d (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]77
變量使調(diào)試器變的容易使用得多妆棒,想不到吧???
流程控制
當(dāng)你通過 Xcode 的源碼編輯器的側(cè)邊槽 (或者通過下面的方法) 插入一個(gè)斷點(diǎn)沸伏,程序到達(dá)斷點(diǎn)時(shí)會(huì)就會(huì)停止運(yùn)行糕珊。
調(diào)試條上會(huì)出現(xiàn)四個(gè)你可以用來控制程序的執(zhí)行流程的按鈕。
從左到右毅糟,四個(gè)按鈕分別是:continue红选,step over,step into姆另,step out喇肋。
第一個(gè),continue 按鈕迹辐,會(huì)取消程序的暫停蝶防,允許程序正常執(zhí)行 (要么一直執(zhí)行下去,要么到達(dá)下一個(gè)斷點(diǎn))明吩。在 LLDB 中间学,你可以使用process continue命令來達(dá)到同樣的效果,它的別名為continue印荔,或者也可以縮寫為c低葫。
第二個(gè),step over 按鈕仍律,會(huì)以黑盒的方式執(zhí)行一行代碼嘿悬。如果所在這行代碼是一個(gè)函數(shù)調(diào)用,那么就不會(huì)跳進(jìn)這個(gè)函數(shù)水泉,而是會(huì)執(zhí)行這個(gè)函數(shù)鹊漠,然后繼續(xù)。LLDB 則可以使用thread step-over茶行,next躯概,或者n命令。
如果你確實(shí)想跳進(jìn)一個(gè)函數(shù)調(diào)用來調(diào)試或者檢查程序的執(zhí)行情況畔师,那就用第三個(gè)按鈕娶靡,step in,或者在LLDB中使用thread step in看锉,step姿锭,或者s命令塔鳍。注意,當(dāng)前行不是函數(shù)調(diào)用時(shí)呻此,next和step效果是一樣的轮纫。
大多數(shù)人知道c,n和s焚鲜,但是其實(shí)還有第四個(gè)按鈕掌唾,step out。如果你曾經(jīng)不小心跳進(jìn)一個(gè)函數(shù)忿磅,但實(shí)際上你想跳過它糯彬,常見的反應(yīng)是重復(fù)的運(yùn)行n直到函數(shù)返回。其實(shí)這種情況葱她,step out 按鈕是你的救世主撩扒。它會(huì)繼續(xù)執(zhí)行到下一個(gè)返回語(yǔ)句 (直到一個(gè)堆棧幀結(jié)束) 然后再次停止。
例子
考慮下面一段程序:
假如我們運(yùn)行程序吨些,讓它停止在斷點(diǎn)搓谆,然后執(zhí)行下面一些列命令:
p i
n
s
p i
finish
p i
frame info
這里,frame info會(huì)告訴你當(dāng)前的行數(shù)和源碼文件豪墅,以及其他一些信息挽拔;查看help frame,help thread和help process來獲得更多信息但校。這一串命令的結(jié)果會(huì)是什么?看答案之前請(qǐng)先想一想啡氢。
(lldb) p i(int) $0 =99(lldb) n2014-11-2210:49:26.445DebuggerDance[60182:4832768]101is odd!(lldb) s(lldb) p i(int) $2 =110(lldb) finish2014-11-2210:49:35.978DebuggerDance[60182:4832768]110is even!(lldb) p i(int) $4 =99(lldb) frame infoframe#0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17
它始終在 17 行的原因是finish命令一直運(yùn)行到isEven()函數(shù)的return状囱,然后立刻停止。注意即使它還在 17 行倘是,其實(shí)這行已經(jīng)被執(zhí)行過了亭枷。
Thread Return
調(diào)試時(shí),還有一個(gè)很棒的函數(shù)可以用來控制程序流程:thread return搀崭。它有一個(gè)可選參數(shù)叨粘,在執(zhí)行時(shí)它會(huì)把可選參數(shù)加載進(jìn)返回寄存器里,然后立刻執(zhí)行返回命令瘤睹,跳出當(dāng)前棧幀升敲。這意味這函數(shù)剩余的部分不會(huì)被執(zhí)行。這會(huì)給 ARC 的引用計(jì)數(shù)造成一些問題轰传,或者會(huì)使函數(shù)內(nèi)的清理部分失效驴党。但是在函數(shù)的開頭執(zhí)行這個(gè)命令,是個(gè)非常好的隔離這個(gè)函數(shù)获茬,偽造返回值的方式 港庄。
讓我們稍微修改一下上面代碼段并運(yùn)行:
p isthreadreturnNOnp even0frame info
看答案前思考一下倔既。下面是答案:
(lldb) p i(int) $0=99(lldb) s(lldb) threadreturnNO(lldb) n(lldb) p even0(BOOL) $2=NO(lldb) frame infoframe#0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17
斷點(diǎn)
我們都把斷點(diǎn)作為一個(gè)停止程序運(yùn)行,檢查當(dāng)前狀態(tài)鹏氧,追蹤 bug 的方式渤涌。但是如果我們改變和斷點(diǎn)交互的方式,很多事情都變成可能把还。
斷點(diǎn)允許控制程序什么時(shí)候停止实蓬,然后允許命令的運(yùn)行。
想象把斷點(diǎn)放在函數(shù)的開頭笨篷,然后用thread return命令重寫函數(shù)的行為瞳秽,然后繼續(xù)。想象一下讓這個(gè)過程自動(dòng)化率翅,聽起來不錯(cuò)练俐,不是嗎?
管理斷點(diǎn)
Xcode 提供了一系列工具來創(chuàng)建和管理斷點(diǎn)冕臭。我們會(huì)一個(gè)個(gè)看過來并介紹 LLDB 中等價(jià)的命令 (是的腺晾,你可以在調(diào)試器內(nèi)部添加斷點(diǎn))。
在 Xcode 的左側(cè)面板辜贵,有一組按鈕悯蝉。其中一個(gè)看起來像斷點(diǎn)。點(diǎn)擊它打開斷點(diǎn)導(dǎo)航托慨,這是一個(gè)可以快速管理所有斷點(diǎn)的面板鼻由。
在這里你可以看到所有的斷點(diǎn) - 在 LLDB 中通過breakpoint list(或者br li) 命令也做同樣的事兒。你也可以點(diǎn)擊單個(gè)斷點(diǎn)來開啟或關(guān)閉 - 在 LLDB 中使用breakpoint enable 和breakpoint disable :
(lldb) br liCurrentbreakpoints:1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line =16, locations =1, resolved =1, hitcount=11.1:where=DebuggerDance`main +27at main.m:16, address =0x000000010a3f6cab, resolved, hitcount=1(lldb) br dis11breakpoints disabled.(lldb) br liCurrentbreakpoints:1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line =16, locations =1Options: disabled1.1:where=DebuggerDance`main +27at main.m:16, address =0x000000010a3f6cab, unresolved, hitcount=1(lldb) br del11breakpoints deleted;0breakpoint locations disabled.(lldb) br liNobreakpoints currentlyset.
創(chuàng)建斷點(diǎn)
在上面的例子中厚棵,我們通過在源碼頁(yè)面器的滾槽16上點(diǎn)擊來創(chuàng)建斷點(diǎn)蕉世。你可以通過把斷點(diǎn)拖拽出滾槽,然后釋放鼠標(biāo)來刪除斷點(diǎn) (消失時(shí)會(huì)有一個(gè)非称庞玻可愛的噗的一下的動(dòng)畫)狠轻。你也可以在斷點(diǎn)導(dǎo)航頁(yè)選擇斷點(diǎn),然后按下刪除鍵刪除彬犯。
要在調(diào)試器中創(chuàng)建斷點(diǎn)向楼,可以使用breakpoint set命令。
(lldb) breakpointset-fmain.m-l16Breakpoint 1:where= DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab
也可以使用縮寫形式br谐区。雖然b是一個(gè)完全不同的命令 (_regexp-break的縮寫)湖蜕,但恰好也可以實(shí)現(xiàn)和上面同樣的效果。
(lldb) b main.m:17Breakpoint2:where=DebuggerDance`main +52at main.m:17, address =0x000000010a3f6cc4
也可以在一個(gè)符號(hào) (C 語(yǔ)言函數(shù)) 上創(chuàng)建斷點(diǎn)宋列,而完全不用指定哪一行
(lldb) b isEvenBreakpoint3:where=DebuggerDance`isEven +16at main.m:4, address =0x000000010a3f6d00(lldb) br s -FisEvenBreakpoint4:where=DebuggerDance`isEven +16at main.m:4, address =0x000000010a3f6d00
這些斷點(diǎn)會(huì)準(zhǔn)確的停止在函數(shù)的開始重荠。Objective-C 的方法也完全可以:
(lldb) breakpointset-F"-[NSArray objectAtIndex:]"Breakpoint 5:where= CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950(lldb) b -[NSArray objectAtIndex:]Breakpoint 6:where= CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950(lldb) breakpointset-F"+[NSSet setWithObject:]"Breakpoint 7:where= CoreFoundation`+[NSSetsetWithObject:], address = 0x000000010abd3820(lldb) b +[NSSetsetWithObject:]Breakpoint 8:where= CoreFoundation`+[NSSetsetWithObject:], address = 0x000000010abd3820
如果想在 Xcode 的UI上創(chuàng)建符號(hào)斷點(diǎn),你可以點(diǎn)擊斷點(diǎn)欄左側(cè)的+按鈕。
然后選擇第三個(gè)選項(xiàng):
這時(shí)會(huì)出現(xiàn)一個(gè)彈出框戈鲁,你可以在里面添加例如-[NSArray objectAtIndex:]這樣的符號(hào)斷點(diǎn)仇参。這樣每次調(diào)用這個(gè)函數(shù)的時(shí)候,程序都會(huì)停止婆殿,不管是你調(diào)用還是蘋果調(diào)用诈乒。
如果你 Xcode 的 UI 上右擊任意斷點(diǎn),然后選擇 "Edit Breakpoint" 的話婆芦,會(huì)有一些非常誘人的選擇怕磨。
這里骏令,斷點(diǎn)已經(jīng)被修改為只有當(dāng)i是99的時(shí)候才會(huì)停止雏节。你也可以使用 "ignore" 選項(xiàng)來告訴斷點(diǎn)最初的n次調(diào)用 (并且條件為真的時(shí)候) 的時(shí)候不要停止。
接下來介紹 'Add Action' 按鈕...
斷點(diǎn)行為 (Action)
上面的例子中榜轿,你或許想知道每一次到達(dá)斷點(diǎn)的時(shí)候i的值或粮。我們可以使用p i作為斷點(diǎn)行為导饲。這樣每次到達(dá)斷點(diǎn)的時(shí)候,都會(huì)自動(dòng)運(yùn)行這個(gè)命令氯材。
你也可以添加多個(gè)行為渣锦,可以是調(diào)試器命令,shell 命令氢哮,也可以是更直接的打哟小:
可以看到它打印i,然后大聲念出那個(gè)句子冗尤,接著打印了自定義的表達(dá)式听盖。
下面是在 LLDB 而不是 Xcode 的 UI 中做這些的時(shí)候,看起來的樣子裂七。
(lldb) breakpointset-F isEvenBreakpoint 1:where= DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00(lldb) breakpoint modify -c'i == 99'1(lldb) breakpointcommandadd 1Enter your debuggercommand(s).? Type'DONE'to end.> p i> DONE(lldb) br li 11: name ='isEven', locations = 1, resolved = 1, hit count = 0? ? Breakpoint commands:? ? ? p iCondition: i == 99? 1.1:where= DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0
接下來說說自動(dòng)化皆看。
賦值后繼續(xù)運(yùn)行
看編輯斷點(diǎn)彈出窗口的底部,你還會(huì)看到一個(gè)選項(xiàng):"Automatically continue after evaluation actions."碍讯。它僅僅是一個(gè)選擇框,但是卻很強(qiáng)大扯躺。選中它捉兴,調(diào)試器會(huì)運(yùn)行你所有的命令,然后繼續(xù)運(yùn)行录语”渡叮看起來就像沒有執(zhí)行任何斷點(diǎn)一樣 (除非斷點(diǎn)太多,運(yùn)行需要一段時(shí)間澎埠,拖慢了你的程序)虽缕。
這個(gè)選項(xiàng)框的效果和讓最后斷點(diǎn)的最后一個(gè)行為是continue一樣。選框只是讓這個(gè)操作變得更簡(jiǎn)單蒲稳。調(diào)試器的輸出是:
(lldb) breakpointset-F isEvenBreakpoint 1:where= DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00(lldb) breakpointcommandadd 1Enter your debuggercommand(s).? Type'DONE'to end.>continue> DONE(lldb) br li 11: name ='isEven', locations = 1, resolved = 1, hit count = 0? ? Breakpoint commands:continue1.1:where= DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0
執(zhí)行斷點(diǎn)后自動(dòng)繼續(xù)運(yùn)行氮趋,允許你完全通過斷點(diǎn)來修改程序伍派!你可以在某一行停止,運(yùn)行一個(gè)expression命令來改變變量剩胁,然后繼續(xù)運(yùn)行诉植。
例子
想想所謂的"打印調(diào)試"技術(shù)吧,不要這么做:
NSLog(@"%@", whatIsInsideThisThing);
而是用個(gè)打印變量的斷點(diǎn)替換 log 語(yǔ)句昵观,然后繼續(xù)運(yùn)行晾腔。
也不要:
intcalculateTheTrickyValue {return9;/*
Figure this out later.
...
}
而是加一個(gè)使用thread return 9命令的斷點(diǎn),然后讓它繼續(xù)運(yùn)行啊犬。
符號(hào)斷點(diǎn)加上 action 真的很強(qiáng)大灼擂。你也可以在你朋友的 Xcode 工程上添加一些斷點(diǎn),并且加上大聲朗讀某些東西的 action觉至√抻Γ看看他們要花多久才能弄明白發(fā)生了什么。??
完全在調(diào)試器內(nèi)運(yùn)行
在開始舞蹈之前康谆,還有一件事要看一看领斥。實(shí)際上你可以在調(diào)試器中執(zhí)行任何 C/Objective-C/C++/Swift 的命令。唯一的缺點(diǎn)就是不能創(chuàng)建新函數(shù)... 這意味著不能創(chuàng)建新的類沃暗,block月洛,函數(shù),有虛擬函數(shù)的 C++ 類等等孽锥。除此之外嚼黔,它都可以做。
我們可以申請(qǐng)分配一些字節(jié):
(lldb) e char *$str = (char *)malloc(8)(lldb) e (void)strcpy($str,"munkeys")(lldb) e $str[1] ='o'(char) $0 ='o'(lldb) p $str(char *) $str =0x00007fd04a900040"monkeys"
我們可以查看內(nèi)存 (使用x命令)惜辑,來看看新數(shù)組中的四個(gè)字節(jié):
(lldb) x/4c $str0x7fd04a900040: monk
我們也可以去掉 3 個(gè)字節(jié) (x命令需要斜引號(hào)唬涧,因?yàn)樗挥幸粋€(gè)內(nèi)存地址的參數(shù),而不是表達(dá)式盛撑;使用help x來獲得更多信息):
(lldb) x/1w`$str + 3`0x7fd04a900043: keys
做完了之后碎节,一定不要忘了釋放內(nèi)存,這樣才不會(huì)內(nèi)存泄露抵卫。(哈狮荔,雖然這是調(diào)試器用到的內(nèi)存):
(lldb) e (void)free($str)
讓我們起舞
現(xiàn)在我們已經(jīng)知道基本的步調(diào)了,是時(shí)候開始跳舞并玩一些瘋狂的事情了介粘。我曾經(jīng)寫過一篇NSArray深度探究的博客殖氏。這篇博客用了很多NSLog語(yǔ)句,但實(shí)際上我的所有探索都是在調(diào)試器中完成的姻采⊙挪桑看看你能不能弄明白怎么做的,這會(huì)是一個(gè)有意思的練習(xí)。
不用斷點(diǎn)調(diào)試
程序運(yùn)行時(shí)婚瓜,Xcode 的調(diào)試條上會(huì)出現(xiàn)暫停按鈕宝鼓,而不是繼續(xù)按鈕:
點(diǎn)擊按鈕會(huì)暫停 app (這會(huì)運(yùn)行process interrupt命令,因?yàn)?LLDB 總是在背后運(yùn)行)闰渔。這會(huì)讓你可以訪問調(diào)試器席函,但看起來可以做的事情不多,因?yàn)樵诋?dāng)前作用域沒有變量冈涧,也沒有特定的代碼讓你看茂附。
這就是有意思的地方。如果你正在運(yùn)行 iOS app督弓,你可以試試這個(gè): (因?yàn)槿肿兞渴强稍L問的)
(lldb) po [[[UIApplicationsharedApplication] keyWindow] recursiveDescription]; layer = >? | >
你可以看到整個(gè)層次营曼。Chisel中pviews就是這么實(shí)現(xiàn)的。
更新UI
有了上面的輸出愚隧,我們可以獲取這個(gè) view:
(lldb) eid$myView = (id)0x7f82b1d01fd0
然后在調(diào)試器中改變它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColorblueColor]]
但是只有程序繼續(xù)運(yùn)行之后才會(huì)看到界面的變化蒂阱。因?yàn)楦淖兊膬?nèi)容必須被發(fā)送到渲染服務(wù)中,然后顯示才會(huì)被更新狂塘。
渲染服務(wù)實(shí)際上是一個(gè)另外的進(jìn)程 (被稱作backboardd)录煤。這就是說即使我們正在調(diào)試的內(nèi)容所在的進(jìn)程被打斷了,backboardd也還是繼續(xù)運(yùn)行著的荞胡。
這意味著你可以運(yùn)行下面的命令妈踊,而不用繼續(xù)運(yùn)行程序:
(lldb) e (void)[CATransactionflush]
即使你仍然在調(diào)試器中,UI 也會(huì)在模擬器或者真機(jī)上實(shí)時(shí)更新泪漂。Chisel為此提供了一個(gè)別名叫做caflush廊营,這個(gè)命令被用來實(shí)現(xiàn)其他的快捷命令,例如hide 萝勤,show 以及其他很多命令露筒。所有Chisel的命令都有文檔,所以安裝后隨意運(yùn)行help show來看更多信息敌卓。
Push 一個(gè) View Controller
想象一個(gè)以UINavigationController為 root ViewController 的應(yīng)用慎式。你可以通過下面的命令,輕松地獲取它:
(lldb) eid$nvc = [[[UIApplicationsharedApplication] keyWindow] rootViewController]
然后 push 一個(gè) child view controller:
(lldb) eid$vc = [UIViewControllernew](lldb) e (void)[[$vc view] setBackgroundColor:[UIColoryellowColor]](lldb) e (void)[$vc setTitle:@"Yay!"](lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
最后運(yùn)行下面的命令:
(lldb) caflush// e (void)[CATransaction flush]
navigation Controller 就會(huì)立刻就被 push 到你眼前趟径。
查找按鈕的 target
想象你在調(diào)試器中有一個(gè)$myButton的變量瘪吏,可以是創(chuàng)建出來的,也可以是從 UI 上抓取出來的舵抹,或者是你停止在斷點(diǎn)時(shí)的一個(gè)局部變量肪虎。你想知道劣砍,按鈕按下的時(shí)候誰會(huì)接收到按鈕發(fā)出的 action惧蛹。非常簡(jiǎn)單:
(lldb) po [$myButtonallTargets]{(? ? )}(lldb) po [$myButtonactionsForTarget:(id)0x7fb58bd2e240forControlEvent:0]<__NSArrayM 0x7fb58bd2aa40>(_handleTap:)
現(xiàn)在你或許想在它發(fā)生的時(shí)候加一個(gè)斷點(diǎn)。在-[MagicEventListener _handleTap:]設(shè)置一個(gè)符號(hào)斷點(diǎn)就可以了,在 Xcode 和 LLDB 中都可以香嗓,然后你就可以點(diǎn)擊按鈕并停在你所希望的地方了迅腔。
觀察實(shí)例變量的變化
假設(shè)你有一個(gè)UIView,不知道為什么它的_layer實(shí)例變量被重寫了 (糟糕)靠娱。因?yàn)橛锌赡懿⒉簧婕暗椒椒ú琢遥覀儾荒苁褂梅?hào)斷點(diǎn)。相反的像云,我們想監(jiān)視什么時(shí)候這個(gè)地址被寫入锌雀。
首先,我們需要找到_layer這個(gè)變量在對(duì)象上的相對(duì)位置:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyViewclass], "_layer"))(ptrdiff_t) $0 =8
現(xiàn)在我們知道($myView + 8)是被寫入的內(nèi)存地址:
(lldb) watchpointsetexpression -- (int *)$myView+ 8Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabledtype= w? ? new value: 0x0000000000000000
這被以wivar $myView _layer加入到Chisel中迅诬。
非重寫方法的符號(hào)斷點(diǎn)
假設(shè)你想知道-[MyViewController viewDidAppear:]什么時(shí)候被調(diào)用腋逆。如果這個(gè)方法并沒有在MyViewController中實(shí)現(xiàn),而是在其父類中實(shí)現(xiàn)的侈贷,該怎么辦呢惩歉?試著設(shè)置一個(gè)斷點(diǎn),會(huì)出現(xiàn)以下結(jié)果:
(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:? Unable to resolve breakpoint to any actual locations.
因?yàn)?LLDB 會(huì)查找一個(gè)符號(hào)俏蛮,但是實(shí)際在這個(gè)類上卻找不到撑蚌,所以斷點(diǎn)也永遠(yuǎn)不會(huì)觸發(fā)。你需要做的是為斷點(diǎn)設(shè)置一個(gè)條件[self isKindOfClass:[MyViewController class]]搏屑,然后把斷點(diǎn)放在UIViewController上争涌。正常情況下這樣設(shè)置一個(gè)條件可以正常工作。但是這里不會(huì)睬棚,因?yàn)槲覀儧]有父類的實(shí)現(xiàn)第煮。
viewDidAppear:是蘋果實(shí)現(xiàn)的方法,因此沒有它的符號(hào)抑党;在方法內(nèi)沒有self包警。如果想在符號(hào)斷點(diǎn)上使用self,你必須知道它在哪里 (它可能在寄存器上底靠,也可能在棧上害晦;在 x86 上,你可以在$esp+4找到它)暑中。但是這是很痛苦的壹瘟,因?yàn)楝F(xiàn)在你必須至少知道四種體系結(jié)構(gòu) (x86,x86-64鳄逾,armv7稻轨,armv64)。想象你需要花多少時(shí)間去學(xué)習(xí)命令集以及它們每一個(gè)的調(diào)用約定雕凹,然后正確的寫一個(gè)在你的超類上設(shè)置斷點(diǎn)并且條件正確的命令殴俱。幸運(yùn)的是政冻,這個(gè)在Chisel被解決了。這被成為bmessage:
(lldb) bmessage -[MyViewController viewDidAppear:]Setting a breakpoint at -[UIViewControllerviewDidAppear:] with condition (void*)object_getClass((id)$rdi) ==0x000000010e2f4d28Breakpoint1: where =UIKit`-[UIViewControllerviewDidAppear:], address =0x000000010e11533c
LLDB 和 Python
LLDB 有內(nèi)建的线欲,完整的Python支持明场。在LLDB中輸入script,會(huì)打開一個(gè) Python REPL李丰。你也可以輸入一行 python 語(yǔ)句作為script 命令的參數(shù)苦锨,這可以運(yùn)行 python 語(yǔ)句而不進(jìn)入REPL:
(lldb) scriptimportos(lldb) script os.system("open http://www.objc.io/")
這樣就允許你創(chuàng)造各種酷的命令。把下面的語(yǔ)句放到文件~/myCommands.py中:
defcaflushCommand(debugger, command, result, internal_dict):? debugger.HandleCommand("e (void)[CATransaction flush]")
然后再 LLDB 中運(yùn)行:
command scriptimport~/myCommands.py
或者把這行命令放在/.lldbinit里趴泌,這樣每次進(jìn)入 LLDB 時(shí)都會(huì)自動(dòng)運(yùn)行舟舒。Chisel其實(shí)就是一個(gè) Python 腳本的集合,這些腳本拼接 (命令) 字符串 嗜憔,然后讓 LLDB 執(zhí)行魏蔗。很簡(jiǎn)單,不是嗎痹筛?
緊握調(diào)試器這一武器
LLDB 可以做的事情很多莺治。大多數(shù)人習(xí)慣于使用p,po帚稠,n谣旁,s和c,但實(shí)際上除此之外滋早,LLDB 可以做的還有很多榄审。掌握所有的命令 (實(shí)際上并不是很多),會(huì)讓你在揭示代碼運(yùn)行時(shí)的運(yùn)行狀態(tài)杆麸,尋找 bug搁进,強(qiáng)制執(zhí)行特定的運(yùn)行路徑時(shí)獲得更大的能力。你甚至可以構(gòu)建簡(jiǎn)單的交互原型 - 比如要是現(xiàn)在以 modal 方式彈出一個(gè) View Controller 會(huì)怎么樣昔头?使用調(diào)試器饼问,一試便知。
這篇文章是為了想你展示 LLDB 的強(qiáng)大之處揭斧,并且鼓勵(lì)你多去探索在控制臺(tái)輸入命令莱革。
打開 LLDB,輸入help讹开,看一看列舉的命令盅视。你嘗試過多少?用了多少旦万?
但愿NSLog看起來不再那么吸引你去用闹击,每次編輯再運(yùn)行并不有趣而且耗時(shí)。
調(diào)試愉快成艘!