iOS LLDB調(diào)試

你是否曾經(jīng)苦惱于理解你的代碼,而去嘗試打印一個(gè)變量的值?

NSLog(@"%@", whatIsInsideThisThing);

或者跳過(guò)一個(gè)函數(shù)調(diào)用來(lái)簡(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ì)地解釋一些你可能還不了解的基本原理,然后展示一系列有趣的例子?,F(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)為合適的步驟來(lái)操作程序的進(jìn)展。(這里有一個(gè)關(guān)于調(diào)試器如何工作的總體的解釋。)

你以前有可能已經(jīng)使用過(guò)調(diào)試器,即使只是在 Xcode 的界面上加一些斷點(diǎn)。但是通過(guò)一些小的技巧,你就可以做一些非??岬氖虑?。GDB to LLDB?參考是一個(gè)非常好的調(diào)試器可用命令的總覽。你也可以安裝?Chisel,它是一個(gè)開源的 LLDB 插件合輯,這會(huì)使調(diào)試變得更加有趣。

與此同時(shí),讓我們以在調(diào)試器中打印變量來(lái)開始我們的旅程吧。

基礎(chǔ)

這里有一個(gè)簡(jiǎn)單的小程序,它會(huì)打印一個(gè)字符串。注意斷點(diǎn)已經(jīng)被加在第 8 行。斷點(diǎn)可以通過(guò)點(diǎn)擊 Xcode 的源碼窗口的側(cè)邊槽進(jìn)行創(chuàng)建。

程序會(huì)在這一行停止運(yùn)行,并且控制臺(tái)會(huì)被打開,允許我們和調(diào)試器交互。那我們應(yīng)該打些什么呢?

help

最簡(jiǎn)單命令是?help,它會(huì)列舉出所有的命令。如果你忘記了一個(gè)命令是做什么的,或者想知道更多的話,你可以通過(guò)?help ?來(lái)了解更多細(xì)節(jié),例如?help print?或者?help thread。如果你甚至忘記了?help?命令是做什么的,你可以試試?help help。不過(guò)你如果知道這么做,那就說(shuō)明你大概還沒(méi)有忘光這個(gè)命令。??

print

打印值很簡(jiǎn)單;只要試試?print?命令:

LLDB 實(shí)際上會(huì)作前綴匹配。所以你也可以使用?prin,pri,或者?p。但你不能使用?pr,因?yàn)?LLDB 不能消除和?process?的歧義 (幸運(yùn)的是?p?并沒(méi)有歧義)。

你可能還注意到了,結(jié)果中有個(gè)?$0。實(shí)際上你可以使用它來(lái)指向這個(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?來(lái)代替?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)單。用?--?來(lái)表征標(biāo)識(shí)的結(jié)束,以及輸入的開始。如果想要?-h?作為標(biāo)識(shí),就用?e -h -- +17,如果想計(jì)算它們的差值,就使用?e -- -h +17。因?yàn)橐话銇?lái)說(shuō)不使用標(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) 的方式來(lái)打印結(jié)果。

(lldb) e -O -- $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)

幸運(yùn)的是,e -o --?有也有個(gè)別名,那就是?po?(print?object 的縮寫),我們可以使用它來(lái)進(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)在讓我們使用一些變量來(lái)減少輸入量。就像你可以在 C 語(yǔ)言中用?int a = 0?來(lái)聲明一個(gè)變量一樣,你也可以在 LLDB 中做同樣的事情。不過(guò)為了能使用聲明的變量,變量必須以美元符開頭。

(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 無(wú)法確定涉及的類型 (譯者注:返回的類型)。這種事情常常發(fā)生,給個(gè)說(shuō)明就好了:

(lldb) p (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]'M'(lldb) p/d (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]77

變量使調(diào)試器變的容易使用得多,想不到吧???

流程控制

當(dāng)你通過(guò) Xcode 的源碼編輯器的側(cè)邊槽 (或者通過(guò)下面的方法) 插入一個(gè)斷點(diǎn),程序到達(dá)斷點(diǎn)時(shí)會(huì)就會(huì)停止運(yùn)行。

調(diào)試條上會(huì)出現(xiàn)四個(gè)你可以用來(lái)控制程序的執(zhí)行流程的按鈕。

從左到右,四個(gè)按鈕分別是:continue,step over,step into,step out。

第一個(gè),continue 按鈕,會(huì)取消程序的暫停,允許程序正常執(zhí)行 (要么一直執(zhí)行下去,要么到達(dá)下一個(gè)斷點(diǎn))。在 LLDB 中,你可以使用?process continue?命令來(lái)達(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)用來(lái)調(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í)際上你想跳過(guò)它,常見的反應(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?來(lái)獲得更多信息。這一串命令的結(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í)行過(guò)了。

Thread Return

調(diào)試時(shí),還有一個(gè)很棒的函數(shù)可以用來(lái)控制程序流程:thread return?。它有一個(gè)可選參數(shù),在執(zhí)行時(shí)它會(huì)把可選參數(shù)加載進(jìn)返回寄存器里,然后立刻執(zhí)行返回命令,跳出當(dāng)前棧幀。這意味這函數(shù)剩余的部分不會(huì)被執(zhí)行。這會(huì)給 ARC 的引用計(jì)數(shù)造成一些問(wèn)題,或者會(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è)過(guò)程自動(dòng)化,聽起來(lái)不錯(cuò),不是嗎?

管理斷點(diǎn)

Xcode 提供了一系列工具來(lái)創(chuàng)建和管理斷點(diǎn)。我們會(huì)一個(gè)個(gè)看過(guò)來(lái)并介紹 LLDB 中等價(jià)的命令 (是的,你可以在調(diào)試器內(nèi)部添加斷點(diǎn))。

在 Xcode 的左側(cè)面板,有一組按鈕。其中一個(gè)看起來(lái)像斷點(diǎn)。點(diǎn)擊它打開斷點(diǎn)導(dǎo)航,這是一個(gè)可以快速管理所有斷點(diǎn)的面板。

在這里你可以看到所有的斷點(diǎn) - 在 LLDB 中通過(guò)?breakpoint list?(或者?br li) 命令也做同樣的事兒。你也可以點(diǎn)擊單個(gè)斷點(diǎn)來(lái)開啟或關(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)

在上面的例子中,我們通過(guò)在源碼頁(yè)面器的滾槽?16?上點(diǎn)擊來(lái)創(chuàng)建斷點(diǎn)。你可以通過(guò)把斷點(diǎn)拖拽出滾槽,然后釋放鼠標(biāo)來(lái)刪除斷點(diǎn) (消失時(shí)會(huì)有一個(gè)非??蓯?ài)的噗的一下的動(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)來(lái)告訴斷點(diǎn)最初的?n次調(diào)用 (并且條件為真的時(shí)候) 的時(shí)候不要停止。

接下來(lái)介紹 '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í)候,看起來(lái)的樣子。

(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

接下來(lái)說(shuō)說(shuō)自動(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)行??雌饋?lái)就像沒(méi)有執(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)行,允許你完全通過(guò)斷點(diǎn)來(lái)修改程序!你可以在某一行停止,運(yùn)行一個(gè)?expression?命令來(lái)改變變量,然后繼續(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??纯此麄円ǘ嗑貌拍芘靼装l(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?命令),來(lái)看看新數(shù)組中的四個(gè)字節(jié):

(lldb) x/4c $str0x7fd04a900040: monk

我們也可以去掉 3 個(gè)字節(jié) (x?命令需要斜引號(hào),因?yàn)樗挥幸粋€(gè)內(nèi)存地址的參數(shù),而不是表達(dá)式;使用?help x?來(lái)獲得更多信息):

(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)寫過(guò)一篇?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ì)讓你可以訪問(wèn)調(diào)試器,但看起來(lái)可以做的事情不多,因?yàn)樵诋?dāng)前作用域沒(méi)有變量,也沒(méi)有特定的代碼讓你看。

這就是有意思的地方。如果你正在運(yùn)行 iOS app,你可以試試這個(gè): (因?yàn)槿肿兞渴强稍L問(wèn)的)

(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)。這就是說(shuō)即使我們正在調(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è)命令被用來(lái)實(shí)現(xiàn)其他的快捷命令,例如?hide ,show ?以及其他很多命令。所有?Chisel?的命令都有文檔,所以安裝后隨意運(yùn)行?help show?來(lái)看更多信息。

Push 一個(gè) View Controller

想象一個(gè)以?UINavigationController?為 root ViewController 的應(yīng)用。你可以通過(guò)下面的命令,輕松地獲取它:

(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)建出來(lái)的,也可以是從 UI 上抓取出來(lái)的,或者是你停止在斷點(diǎn)時(shí)的一個(gè)局部變量。你想知道,按鈕按下的時(shí)候誰(shuí)會(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è)方法并沒(méi)有在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)槲覀儧](méi)有父類的實(shí)現(xiàn)。

viewDidAppear:?是蘋果實(shí)現(xiàn)的方法,因此沒(méi)有它的符號(hào);在方法內(nèi)沒(mé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,看一看列舉的命令。你嘗試過(guò)多少?用了多少?

但愿?NSLog?看起來(lái)不再那么吸引你去用,每次編輯再運(yùn)行并不有趣而且耗時(shí)。

調(diào)試愉快!

參考:https://objccn.io/issue-19-2/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容