你是否曾經(jīng)苦惱于理解你的代碼晒衩,而去嘗試打印一個變量的值逛绵?
NSLog(@"%@", whatIsInsideThisThing);
或者跳過一個函數(shù)調用來簡化程序的行為?
NSNumber *n = @7;//實際應該調用這個函數(shù):Foo();
或者短路一個邏輯檢查频伤?
if(1||theBooleanAtStake) { ... }
或者偽造一個函數(shù)實現(xiàn)恳谎?
intcalculateTheTrickyValue {return9;/*
先這么著
...
}
并且每次必須重新編譯,從頭開始憋肖?
構建軟件是復雜的因痛,并且 Bug 總會出現(xiàn)。一個常見的修復周期就是修改代碼岸更,編譯鸵膏,重新運行,并且祈禱出現(xiàn)最好的結果怎炊。
但是不一定要這么做谭企。你可以使用調試器。而且即使你已經(jīng)知道如何使用調試器檢查變量评肆,它可以做的還有很多债查。
這篇文章將試圖挑戰(zhàn)你對調試的認知,并詳細地解釋一些你可能還不了解的基本原理瓜挽,然后展示一系列有趣的例子№锿ⅲ現(xiàn)在就讓我們開始與調試器共舞一曲華爾茲,看看最后能達到怎樣的高度久橙。
LLDB
LLDB是一個有著 REPL 的特性和 C++ ,Python 插件的開源調試器俄占。LLDB 綁定在 Xcode 內部,存在于主窗口底部的控制臺中剥汤。調試器允許你在程序運行的特定時暫停它颠放,你可以查看變量的值,執(zhí)行自定的指令吭敢,并且按照你所認為合適的步驟來操作程序的進展。(這里有一個關于調試器如何工作的總體的解釋暮芭。)
你以前有可能已經(jīng)使用過調試器鹿驼,即使只是在 Xcode 的界面上加一些斷點。但是通過一些小的技巧辕宏,你就可以做一些非承笪酷的事情。GDB to LLDB參考是一個非常好的調試器可用命令的總覽瑞筐。你也可以安裝Chisel凄鼻,它是一個開源的 LLDB 插件合輯,這會使調試變得更加有趣。
與此同時块蚌,讓我們以在調試器中打印變量來開始我們的旅程吧闰非。
基礎
這里有一個簡單的小程序,它會打印一個字符串峭范。注意斷點已經(jīng)被加在第 8 行财松。斷點可以通過點擊 Xcode 的源碼窗口的側邊槽進行創(chuàng)建。
程序會在這一行停止運行纱控,并且控制臺會被打開辆毡,允許我們和調試器交互。那我們應該打些什么呢甜害?
help
最簡單命令是help舶掖,它會列舉出所有的命令。如果你忘記了一個命令是做什么的尔店,或者想知道更多的話访锻,你可以通過help 來了解更多細節(jié),例如help print或者help thread闹获。如果你甚至忘記了help命令是做什么的期犬,你可以試試help help。不過你如果知道這么做避诽,那就說明你大概還沒有忘光這個命令龟虎。??
打印值很簡單;只要試試print命令:
LLDB 實際上會作前綴匹配沙庐。所以你也可以使用prin鲤妥,pri,或者p拱雏。但你不能使用pr棉安,因為 LLDB 不能消除和process的歧義 (幸運的是p并沒有歧義)。
你可能還注意到了铸抑,結果中有個$0贡耽。實際上你可以使用它來指向這個結果。試試print $0 + 7鹊汛,你會看到106蒲赂。任何以美元符開頭的東西都是存在于 LLDB 的命名空間的,它們是為了幫助你進行調試而存在的刁憋。
expression
如果想改變一個值怎么辦滥嘴?你或許會猜modify。其實這時候我們要用到的是expression這個方便的命令至耻。
這不僅會改變調試器中的值若皱,實際上它改變了程序中的值镊叁。這時候繼續(xù)執(zhí)行程序,將會打印42 red balloons走触。神奇吧晦譬。
注意,從現(xiàn)在開始饺汹,我們將會偷懶分別以p和e來代替print和expression蛔添。
什么是print命令
考慮一個有意思的表達式:p count = 18。如果我們運行這條命令兜辞,然后打印count的內容迎瞧。我們將看到它的結果與expression count = 18一樣。
和expression不同的是逸吵,print命令不需要參數(shù)凶硅。比如e -h +17中,你很難區(qū)分到底是以-h為標識扫皱,僅僅執(zhí)行+17呢足绅,還是要計算17和h的差值。連字符號確實很讓人困惑韩脑,你或許得不到自己想要的結果氢妈。
幸運的是,解決方案很簡單段多。用--來表征標識的結束首量,以及輸入的開始。如果想要-h作為標識进苍,就用e -h -- +17加缘,如果想計算它們的差值,就使用e -- -h +17觉啊。因為一般來說不使用標識的情況比較多拣宏,所以e --就有了一個簡寫的方式,那就是print杠人。
輸入help print勋乾,然后向下滾動,你會發(fā)現(xiàn):
'print'is an abbreviationfor'expression --'.? (print是`expression --`的縮寫)
打印對象
嘗試輸入
p objects
輸出會有點啰嗦
(NSString *) $7 =0x0000000104da4040@"red balloons"
如果我們嘗試打印結構更復雜的對象搜吧,結果甚至會更糟
(lldb) p @[ @"foo", @"bar"](NSArray *) $8 =0x00007fdb9b71b3e0@"2 objects"
實際上市俊,我們想看的是對象的description方法的結果。我么需要使用-O(字母 O滤奈,而不是數(shù)字 0) 標志告訴expression命令以對象(Object) 的方式來打印結果。
(lldb) e -O -- $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)
幸運的是撩满,e -o --有也有個別名蜒程,那就是po(printobject 的縮寫)绅你,我們可以使用它來進行簡化:
(lldb) po $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)(lldb) po @"lunar"lunar(lldb) p @"lunar"(NSString *) $13 =0x00007fdb9d0003b0@"lunar"
打印變量
可以給print指定不同的打印格式。它們都是以print/或者簡化的p/格式書寫昭躺。下面是一些例子:
默認的格式
(lldb) p 16
16
十六進制:
(lldb) p/x 16
0x10
二進制 (t代表two):
(lldb) p/t160b00000000000000000000000000010000(lldb) p/t (char)160b00010000
你也可以使用p/c打印字符忌锯,或者p/s打印以空終止的字符串 (譯者注:以 '\0' 結尾的字符串)。
這里是格式的完整清單领炫。
變量
現(xiàn)在你已經(jīng)可以打印對象和簡單類型偶垮,并且知道如何使用expression命令在調試器中修改它們了。現(xiàn)在讓我們使用一些變量來減少輸入量帝洪。就像你可以在 C 語言中用int a = 0來聲明一個變量一樣似舵,你也可以在 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ā)生蛛芥,給個說明就好了:
(lldb) p (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]'M'(lldb) p/d (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]77
變量使調試器變的容易使用得多,想不到吧军援???
流程控制
當你通過 Xcode 的源碼編輯器的側邊槽 (或者通過下面的方法) 插入一個斷點仅淑,程序到達斷點時會就會停止運行。
調試條上會出現(xiàn)四個你可以用來控制程序的執(zhí)行流程的按鈕胸哥。
從左到右涯竟,四個按鈕分別是:continue,step over烘嘱,step into昆禽,step out。
第一個蝇庭,continue 按鈕醉鳖,會取消程序的暫停,允許程序正常執(zhí)行 (要么一直執(zhí)行下去哮内,要么到達下一個斷點)盗棵。在 LLDB 中,你可以使用process continue命令來達到同樣的效果北发,它的別名為continue纹因,或者也可以縮寫為c。
第二個琳拨,step over 按鈕瞭恰,會以黑盒的方式執(zhí)行一行代碼。如果所在這行代碼是一個函數(shù)調用狱庇,那么就不會跳進這個函數(shù)惊畏,而是會執(zhí)行這個函數(shù)恶耽,然后繼續(xù)。LLDB 則可以使用thread step-over颜启,next偷俭,或者n命令。
如果你確實想跳進一個函數(shù)調用來調試或者檢查程序的執(zhí)行情況缰盏,那就用第三個按鈕涌萤,step in,或者在LLDB中使用thread step in口猜,step负溪,或者s命令。注意暮的,當前行不是函數(shù)調用時笙以,next和step效果是一樣的。
大多數(shù)人知道c冻辩,n和s猖腕,但是其實還有第四個按鈕,step out恨闪。如果你曾經(jīng)不小心跳進一個函數(shù)倘感,但實際上你想跳過它,常見的反應是重復的運行n直到函數(shù)返回咙咽。其實這種情況老玛,step out 按鈕是你的救世主。它會繼續(xù)執(zhí)行到下一個返回語句 (直到一個堆棧幀結束) 然后再次停止钧敞。
例子
考慮下面一段程序:
假如我們運行程序蜡豹,讓它停止在斷點,然后執(zhí)行下面一些列命令:
p i
n
s
p i
finish
p i
frame info
這里溉苛,frame info會告訴你當前的行數(shù)和源碼文件镜廉,以及其他一些信息;查看help frame塔插,help thread和help process來獲得更多信息想许。這一串命令的結果會是什么捧颅?看答案之前請先想一想亮蒋。
(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命令一直運行到isEven()函數(shù)的return趁怔,然后立刻停止铺浇。注意即使它還在 17 行,其實這行已經(jīng)被執(zhí)行過了。
Thread Return
調試時,還有一個很棒的函數(shù)可以用來控制程序流程:thread return敞咧。它有一個可選參數(shù),在執(zhí)行時它會把可選參數(shù)加載進返回寄存器里,然后立刻執(zhí)行返回命令,跳出當前棧幀粘招。這意味這函數(shù)剩余的部分不會被執(zhí)行逊笆。這會給 ARC 的引用計數(shù)造成一些問題症虑,或者會使函數(shù)內的清理部分失效。但是在函數(shù)的開頭執(zhí)行這個命令氯葬,是個非常好的隔離這個函數(shù),偽造返回值的方式 瞻坝。
讓我們稍微修改一下上面代碼段并運行:
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
斷點
我們都把斷點作為一個停止程序運行,檢查當前狀態(tài)吕晌,追蹤 bug 的方式淫茵。但是如果我們改變和斷點交互的方式翁都,很多事情都變成可能贼涩。
斷點允許控制程序什么時候停止,然后允許命令的運行嗡贺。
想象把斷點放在函數(shù)的開頭愉棱,然后用thread return命令重寫函數(shù)的行為,然后繼續(xù)。想象一下讓這個過程自動化钦扭,聽起來不錯,不是嗎?
管理斷點
Xcode 提供了一系列工具來創(chuàng)建和管理斷點。我們會一個個看過來并介紹 LLDB 中等價的命令 (是的断凶,你可以在調試器內部添加斷點)。
在 Xcode 的左側面板巫俺,有一組按鈕认烁。其中一個看起來像斷點。點擊它打開斷點導航介汹,這是一個可以快速管理所有斷點的面板却嗡。
在這里你可以看到所有的斷點 - 在 LLDB 中通過breakpoint list(或者br li) 命令也做同樣的事兒。你也可以點擊單個斷點來開啟或關閉 - 在 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)建斷點
在上面的例子中嘹承,我們通過在源碼頁面器的滾槽16上點擊來創(chuàng)建斷點窗价。你可以通過把斷點拖拽出滾槽,然后釋放鼠標來刪除斷點 (消失時會有一個非掣献可愛的噗的一下的動畫)舌镶。你也可以在斷點導航頁選擇斷點,然后按下刪除鍵刪除豪娜。
要在調試器中創(chuàng)建斷點,可以使用breakpoint set命令哟楷。
(lldb) breakpointset-fmain.m-l16Breakpoint 1:where= DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab
也可以使用縮寫形式br瘤载。雖然b是一個完全不同的命令 (_regexp-break的縮寫),但恰好也可以實現(xiàn)和上面同樣的效果卖擅。
(lldb) b main.m:17Breakpoint2:where=DebuggerDance`main +52at main.m:17, address =0x000000010a3f6cc4
也可以在一個符號 (C 語言函數(shù)) 上創(chuàng)建斷點鸣奔,而完全不用指定哪一行
(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
這些斷點會準確的停止在函數(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)建符號斷點惩阶,你可以點擊斷點欄左側的+按鈕挎狸。
然后選擇第三個選項:
這時會出現(xiàn)一個彈出框,你可以在里面添加例如-[NSArray objectAtIndex:]這樣的符號斷點断楷。這樣每次調用這個函數(shù)的時候锨匆,程序都會停止,不管是你調用還是蘋果調用冬筒。
如果你 Xcode 的 UI 上右擊任意斷點恐锣,然后選擇 "Edit Breakpoint" 的話,會有一些非常誘人的選擇舞痰。
這里土榴,斷點已經(jīng)被修改為只有當i是99的時候才會停止。你也可以使用 "ignore" 選項來告訴斷點最初的n次調用 (并且條件為真的時候) 的時候不要停止响牛。
接下來介紹 'Add Action' 按鈕...
斷點行為 (Action)
上面的例子中玷禽,你或許想知道每一次到達斷點的時候i的值赫段。我們可以使用p i作為斷點行為。這樣每次到達斷點的時候矢赁,都會自動運行這個命令糯笙。
你也可以添加多個行為,可以是調試器命令坯台,shell 命令炬丸,也可以是更直接的打印:
可以看到它打印i蜒蕾,然后大聲念出那個句子稠炬,接著打印了自定義的表達式。
下面是在 LLDB 而不是 Xcode 的 UI 中做這些的時候咪啡,看起來的樣子首启。
(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
接下來說說自動化。
賦值后繼續(xù)運行
看編輯斷點彈出窗口的底部撤摸,你還會看到一個選項:"Automatically continue after evaluation actions."毅桃。它僅僅是一個選擇框,但是卻很強大准夷。選中它钥飞,調試器會運行你所有的命令,然后繼續(xù)運行衫嵌《林妫看起來就像沒有執(zhí)行任何斷點一樣 (除非斷點太多,運行需要一段時間楔绞,拖慢了你的程序)结闸。
這個選項框的效果和讓最后斷點的最后一個行為是continue一樣。選框只是讓這個操作變得更簡單酒朵。調試器的輸出是:
(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í)行斷點后自動繼續(xù)運行桦锄,允許你完全通過斷點來修改程序!你可以在某一行停止蔫耽,運行一個expression命令來改變變量结耀,然后繼續(xù)運行。
例子
想想所謂的"打印調試"技術吧针肥,不要這么做:
NSLog(@"%@", whatIsInsideThisThing);
而是用個打印變量的斷點替換 log 語句饼记,然后繼續(xù)運行。
也不要:
intcalculateTheTrickyValue {return9;/*
Figure this out later.
...
}
而是加一個使用thread return 9命令的斷點慰枕,然后讓它繼續(xù)運行具则。
符號斷點加上 action 真的很強大。你也可以在你朋友的 Xcode 工程上添加一些斷點具帮,并且加上大聲朗讀某些東西的 action博肋〉驼看看他們要花多久才能弄明白發(fā)生了什么。??
完全在調試器內運行
在開始舞蹈之前匪凡,還有一件事要看一看膊畴。實際上你可以在調試器中執(zhí)行任何 C/Objective-C/C++/Swift 的命令。唯一的缺點就是不能創(chuàng)建新函數(shù)... 這意味著不能創(chuàng)建新的類病游,block唇跨,函數(shù),有虛擬函數(shù)的 C++ 類等等衬衬。除此之外买猖,它都可以做。
我們可以申請分配一些字節(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"
我們可以查看內存 (使用x命令)滋尉,來看看新數(shù)組中的四個字節(jié):
(lldb) x/4c $str0x7fd04a900040: monk
我們也可以去掉 3 個字節(jié) (x命令需要斜引號玉控,因為它只有一個內存地址的參數(shù),而不是表達式狮惜;使用help x來獲得更多信息):
(lldb) x/1w`$str + 3`0x7fd04a900043: keys
做完了之后高诺,一定不要忘了釋放內存,這樣才不會內存泄露碾篡。(哈虱而,雖然這是調試器用到的內存):
(lldb) e (void)free($str)
讓我們起舞
現(xiàn)在我們已經(jīng)知道基本的步調了,是時候開始跳舞并玩一些瘋狂的事情了开泽。我曾經(jīng)寫過一篇NSArray深度探究的博客薛窥。這篇博客用了很多NSLog語句,但實際上我的所有探索都是在調試器中完成的眼姐。看看你能不能弄明白怎么做的佩番,這會是一個有意思的練習众旗。
不用斷點調試
程序運行時,Xcode 的調試條上會出現(xiàn)暫停按鈕趟畏,而不是繼續(xù)按鈕:
點擊按鈕會暫停 app (這會運行process interrupt命令贡歧,因為 LLDB 總是在背后運行)。這會讓你可以訪問調試器赋秀,但看起來可以做的事情不多利朵,因為在當前作用域沒有變量,也沒有特定的代碼讓你看猎莲。
這就是有意思的地方绍弟。如果你正在運行 iOS app,你可以試試這個: (因為全局變量是可訪問的)
(lldb) po [[[UIApplicationsharedApplication] keyWindow] recursiveDescription]; layer = >? | >
你可以看到整個層次著洼。Chisel中pviews就是這么實現(xiàn)的樟遣。
更新UI
有了上面的輸出而叼,我們可以獲取這個 view:
(lldb) eid$myView = (id)0x7f82b1d01fd0
然后在調試器中改變它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColorblueColor]]
但是只有程序繼續(xù)運行之后才會看到界面的變化。因為改變的內容必須被發(fā)送到渲染服務中豹悬,然后顯示才會被更新葵陵。
渲染服務實際上是一個另外的進程 (被稱作backboardd)。這就是說即使我們正在調試的內容所在的進程被打斷了瞻佛,backboardd也還是繼續(xù)運行著的脱篙。
這意味著你可以運行下面的命令,而不用繼續(xù)運行程序:
(lldb) e (void)[CATransactionflush]
即使你仍然在調試器中伤柄,UI 也會在模擬器或者真機上實時更新绊困。Chisel為此提供了一個別名叫做caflush,這個命令被用來實現(xiàn)其他的快捷命令响迂,例如hide 考抄,show 以及其他很多命令。所有Chisel的命令都有文檔蔗彤,所以安裝后隨意運行help show來看更多信息川梅。
Push 一個 View Controller
想象一個以UINavigationController為 root ViewController 的應用。你可以通過下面的命令然遏,輕松地獲取它:
(lldb) eid$nvc = [[[UIApplicationsharedApplication] keyWindow] rootViewController]
然后 push 一個 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]
最后運行下面的命令:
(lldb) caflush// e (void)[CATransaction flush]
navigation Controller 就會立刻就被 push 到你眼前贫途。
查找按鈕的 target
想象你在調試器中有一個$myButton的變量,可以是創(chuàng)建出來的待侵,也可以是從 UI 上抓取出來的丢早,或者是你停止在斷點時的一個局部變量。你想知道秧倾,按鈕按下的時候誰會接收到按鈕發(fā)出的 action怨酝。非常簡單:
(lldb) po [$myButtonallTargets]{(? ? )}(lldb) po [$myButtonactionsForTarget:(id)0x7fb58bd2e240forControlEvent:0]<__NSArrayM 0x7fb58bd2aa40>(_handleTap:)
現(xiàn)在你或許想在它發(fā)生的時候加一個斷點。在-[MagicEventListener _handleTap:]設置一個符號斷點就可以了那先,在 Xcode 和 LLDB 中都可以农猬,然后你就可以點擊按鈕并停在你所希望的地方了。
觀察實例變量的變化
假設你有一個UIView售淡,不知道為什么它的_layer實例變量被重寫了 (糟糕)斤葱。因為有可能并不涉及到方法,我們不能使用符號斷點揖闸。相反的揍堕,我們想監(jiān)視什么時候這個地址被寫入。
首先汤纸,我們需要找到_layer這個變量在對象上的相對位置:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyViewclass], "_layer"))(ptrdiff_t) $0 =8
現(xiàn)在我們知道($myView + 8)是被寫入的內存地址:
(lldb) watchpointsetexpression -- (int *)$myView+ 8Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabledtype= w? ? new value: 0x0000000000000000
這被以wivar $myView _layer加入到Chisel中衩茸。
非重寫方法的符號斷點
假設你想知道-[MyViewController viewDidAppear:]什么時候被調用。如果這個方法并沒有在MyViewController中實現(xiàn)蹲嚣,而是在其父類中實現(xiàn)的递瑰,該怎么辦呢祟牲?試著設置一個斷點,會出現(xiàn)以下結果:
(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:? Unable to resolve breakpoint to any actual locations.
因為 LLDB 會查找一個符號抖部,但是實際在這個類上卻找不到说贝,所以斷點也永遠不會觸發(fā)。你需要做的是為斷點設置一個條件[self isKindOfClass:[MyViewController class]]慎颗,然后把斷點放在UIViewController上乡恕。正常情況下這樣設置一個條件可以正常工作。但是這里不會俯萎,因為我們沒有父類的實現(xiàn)傲宜。
viewDidAppear:是蘋果實現(xiàn)的方法,因此沒有它的符號夫啊;在方法內沒有self函卒。如果想在符號斷點上使用self,你必須知道它在哪里 (它可能在寄存器上撇眯,也可能在棧上报嵌;在 x86 上,你可以在$esp+4找到它)熊榛。但是這是很痛苦的锚国,因為現(xiàn)在你必須至少知道四種體系結構 (x86,x86-64玄坦,armv7血筑,armv64)。想象你需要花多少時間去學習命令集以及它們每一個的調用約定煎楣,然后正確的寫一個在你的超類上設置斷點并且條件正確的命令豺总。幸運的是,這個在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 有內建的园欣,完整的Python支持。在LLDB中輸入script休蟹,會打開一個 Python REPL。你也可以輸入一行 python 語句作為script 命令的參數(shù)日矫,這可以運行 python 語句而不進入REPL:
(lldb) scriptimportos(lldb) script os.system("open http://www.objc.io/")
這樣就允許你創(chuàng)造各種酷的命令赂弓。把下面的語句放到文件~/myCommands.py中:
defcaflushCommand(debugger, command, result, internal_dict):? debugger.HandleCommand("e (void)[CATransaction flush]")
然后再 LLDB 中運行:
command scriptimport~/myCommands.py
或者把這行命令放在/.lldbinit里,這樣每次進入 LLDB 時都會自動運行哪轿。Chisel其實就是一個 Python 腳本的集合盈魁,這些腳本拼接 (命令) 字符串 ,然后讓 LLDB 執(zhí)行窃诉。很簡單杨耙,不是嗎赤套?
緊握調試器這一武器
LLDB 可以做的事情很多。大多數(shù)人習慣于使用p珊膜,po容握,n,s和c车柠,但實際上除此之外剔氏,LLDB 可以做的還有很多。掌握所有的命令 (實際上并不是很多)竹祷,會讓你在揭示代碼運行時的運行狀態(tài)谈跛,尋找 bug,強制執(zhí)行特定的運行路徑時獲得更大的能力塑陵。你甚至可以構建簡單的交互原型 - 比如要是現(xiàn)在以 modal 方式彈出一個 View Controller 會怎么樣感憾?使用調試器,一試便知令花。
這篇文章是為了想你展示 LLDB 的強大之處阻桅,并且鼓勵你多去探索在控制臺輸入命令。
打開 LLDB彭则,輸入help鳍刷,看一看列舉的命令。你嘗試過多少俯抖?用了多少输瓜?
但愿NSLog看起來不再那么吸引你去用,每次編輯再運行并不有趣而且耗時芬萍。
調試愉快尤揣!