你是否曾經苦惱於理解你的代碼,而去嘗試打印一個變量的值?
NSLog(@"%@", whatIsInsideThisThing);
或者跳過一個函數調用來簡化程序的行為?
NSNumber *n = @7; // 實際應該調用這個函數:Foo();
或者短路一個邏輯檢查?
if (1 || theBooleanAtStake) { ... }
或者偽造一個函數實現?
int calculateTheTrickyValue { return 9; /* 先這么着 ... }
並且每次必須重新編譯,從頭開始?
構建軟件是復雜的,並且 Bug 總會出現。一個常見的修復周期就是修改代碼,編譯,重新運行,並且祈禱出現最好的結果。
但是不一定要這么做。你可以使用調試器。而且即使你已經知道如何使用調試器檢查變量,它可以做的還有很多。
這篇文章將試圖挑戰你對調試的認知,並詳細地解釋一些你可能還不了解的基本原理,然后展示一系列有趣的例子。現在就讓我們開始與調試器共舞一曲華爾茲,看看最后能達到怎樣的高度。
LLDB
LLDB 是一個有着 REPL 的特性和 C++ ,Python 插件的開源調試器。LLDB 綁定在 Xcode 內部,存在於主窗口底部的控制台中。調試器允許你在程序運行的特定時暫停它,你可以查看變量的值,執行自定的指令,並且按照你所認為合適的步驟來操作程序的進展。
你以前有可能已經使用過調試器,即使只是在 Xcode 的界面上加一些斷點。但是通過一些小的技巧,你就可以做一些非常酷的事情。GDB to LLDB 參考是一個非常好的調試器可用命令的總覽。你也可以安裝 Chisel,它是一個開源的 LLDB 插件合輯,這會使調試變得更加有趣。
與此同時,讓我們以在調試器中打印變量來開始我們的旅程吧。
基礎
這里有一個簡單的小程序,它會打印一個字符串。注意斷點已經被加在第 8 行。斷點可以通過點擊 Xcode 的源碼窗口的側邊槽進行創建。
程序會在這一行停止運行,並且控制台會被打開,允許我們和調試器交互。那我們應該打些什么呢?
help
最簡單命令是 help
,它會列舉出所有的命令。如果你忘記了一個命令是做什么的,或者想知道更多的話,你可以通過 help <command>
來了解更多細節,例如 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
這個方便的命令。
這不僅會改變調試器中的值,實際上它改變了程序中的值。這時候繼續執行程序,將會打印 42 red balloons
。神奇吧。
注意,從現在開始,我們將會偷懶分別以 p
和 e
來代替 print
和 expression
。
什么是 print 命令
考慮一個有意思的表達式:p count = 18
。如果我們運行這條命令,然后打印 count
的內容。我們將看到它的結果與 expression count = 18
一樣。
和 expression
不同的是,print
命令不需要參數。比如 e -h +17
中,你很難區分到底是以 -h
為標識,僅僅執行 +17
呢,還是要計算 17
和 h
的差值。連字符號確實很讓人困惑,你或許得不到自己想要的結果。
幸運的是,解決方案很簡單。用 --
來表征標識的結束,以及輸入的開始。如果想要 -h
作為標識,就用 e -h -- +17
,如果想計算它們的差值,就使用 e -- -h +17
。因為一般來說不使用標識的情況比較多,所以 e --
就有了一個簡寫的方式,那就是 print
。
輸入 help print
,然后向下滾動,你會發現:
'print' is an abbreviation for 'expression --'. (print是 `expression --` 的縮寫)
打印對象
嘗試輸入
p objects
輸出會有點啰嗦
(NSString *) $7 = 0x0000000104da4040 @"red balloons"
如果我們嘗試打印結構更復雜的對象,結果甚至會更糟
(lldb) p @[ @"foo", @"bar" ] (NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"
實際上,我們想看的是對象的 description
方法的結果。我么需要使用 -O
(字母 O,而不是數字 0) 標志告訴 expression
命令以 對象
(Object) 的方式來打印結果。
(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>( foo, bar )
幸運的是,e -o --
有也有個別名,那就是 po
(print object 的縮寫),我們可以使用它來進行簡化:
(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>( foo, bar ) (lldb) po @"lunar" lunar (lldb) p @"lunar" (NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
打印變量
可以給 print
指定不同的打印格式。它們都是以 print/<fmt>
或者簡化的 p/<fmt>
格式書寫。下面是一些例子:
默認的格式
(lldb) p 16
16
十六進制:
(lldb) p/x 16
0x10
二進制 (t
代表 two):
(lldb) p/t 16 0b00000000000000000000000000010000 (lldb) p/t (char)16 0b00010000
你也可以使用 p/c
打印字符,或者 p/s
打印以空終止的字符串 (譯者注:以 '\0' 結尾的字符串)。
這里是格式的完整清單。
變量
現在你已經可以打印對象和簡單類型,並且知道如何使用 expression
命令在調試器中修改它們了。現在讓我們使用一些變量來減少輸入量。就像你可以在 C 語言中用 int a = 0
來聲明一個變量一樣,你也可以在 LLDB 中做同樣的事情。不過為了能使用聲明的變量,變量必須以美元符開頭。
(lldb) e int $a = 2 (lldb) p $a * 19 38 (lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ] (lldb) p [$array count] 2 (lldb) po [[$array objectAtIndex:0] uppercaseString] SATURDAY (lldb) p [[$array objectAtIndex:$a] characterAtIndex:0] error: no known method '-characterAtIndex:'; cast the message send to the method's return type error: 1 errors parsing expression
悲劇了,LLDB 無法確定涉及的類型 (譯者注:返回的類型)。這種事情常常發生,給個說明就好了:
(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0] 'M' (lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0] 77
變量使調試器變的容易使用得多,想不到吧?😉
流程控制
當你通過 Xcode 的源碼編輯器的側邊槽 (或者通過下面的方法) 插入一個斷點,程序到達斷點時會就會停止運行。
調試條上會出現四個你可以用來控制程序的執行流程的按鈕。
從左到右,四個按鈕分別是:continue,step over,step into,step out。
第一個,continue 按鈕,會取消程序的暫停,允許程序正常執行 (要么一直執行下去,要么到達下一個斷點)。在 LLDB 中,你可以使用 process continue
命令來達到同樣的效果,它的別名為 continue
,或者也可以縮寫為 c
。
第二個,step over 按鈕,會以黑盒的方式執行一行代碼。如果所在這行代碼是一個函數調用,那么就不會跳進這個函數,而是會執行這個函數,然后繼續。LLDB 則可以使用 thread step-over
,next
,或者 n
命令。
如果你確實想跳進一個函數調用來調試或者檢查程序的執行情況,那就用第三個按鈕,step in,或者在LLDB中使用 thread step in
,step
,或者 s
命令。注意,當前行不是函數調用時,next
和 step
效果是一樣的。
大多數人知道 c
,n
和 s
,但是其實還有第四個按鈕,step out。如果你曾經不小心跳進一個函數,但實際上你想跳過它,常見的反應是重復的運行 n
直到函數返回。其實這種情況,step out 按鈕是你的救世主。它會繼續執行到下一個返回語句 (直到一個堆棧幀結束) 然后再次停止。
例子
考慮下面一段程序:
假如我們運行程序,讓它停止在斷點,然后執行下面一些列命令:
p i
n
s
p i
finish
p i
frame info
這里,frame info
會告訴你當前的行數和源碼文件,以及其他一些信息;查看 help frame
,help thread
和 help process
來獲得更多信息。這一串命令的結果會是什么?看答案之前請先想一想。
(lldb) p i
(int) $0 = 99 (lldb) n 2014-11-22 10:49:26.445 DebuggerDance[60182:4832768] 101 is odd! (lldb) s (lldb) p i (int) $2 = 110 (lldb) finish 2014-11-22 10:49:35.978 DebuggerDance[60182:4832768] 110 is even! (lldb) p i (int) $4 = 99 (lldb) frame info frame #0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17
它始終在 17 行的原因是 finish
命令一直運行到 isEven()
函數的 return
,然后立刻停止。注意即使它還在 17 行,其實這行已經被執行過了。
Thread Return
調試時,還有一個很棒的函數可以用來控制程序流程:thread return
。它有一個可選參數,在執行時它會把可選參數加載進返回寄存器里,然后立刻執行返回命令,跳出當前棧幀。這意味這函數剩余的部分不會被執行。這會給 ARC 的引用計數造成一些問題,或者會使函數內的清理部分失效。但是在函數的開頭執行這個命令,是個非常好的隔離這個函數,偽造返回值的方式 。
讓我們稍微修改一下上面代碼段並運行:
p i
s
thread return NO n p even0 frame info
看答案前思考一下。下面是答案:
(lldb) p i
(int) $0 = 99 (lldb) s (lldb) thread return NO (lldb) n (lldb) p even0 (BOOL) $2 = NO (lldb) frame info frame #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17
斷點
我們都把斷點作為一個停止程序運行,檢查當前狀態,追蹤 bug 的方式。但是如果我們改變和斷點交互的方式,很多事情都變成可能。
斷點允許控制程序什么時候停止,然后允許命令的運行。
想象把斷點放在函數的開頭,然后用 thread return
命令重寫函數的行為,然后繼續。想象一下讓這個過程自動化,聽起來不錯,不是嗎?
管理斷點
Xcode 提供了一系列工具來創建和管理斷點。我們會一個個看過來並介紹 LLDB 中等價的命令 (是的,你可以在調試器內部添加斷點)。
在 Xcode 的左側面板,有一組按鈕。其中一個看起來像斷點。點擊它打開斷點導航,這是一個可以快速管理所有斷點的面板。
在這里你可以看到所有的斷點 - 在 LLDB 中通過 breakpoint list
(或者 br li
) 命令也做同樣的事兒。你也可以點擊單個斷點來開啟或關閉 - 在 LLDB 中使用 breakpoint enable <breakpointID>
和 breakpoint disable <breakpointID>
:
(lldb) br li
Current breakpoints: 1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1, resolved = 1, hit count = 1 1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, resolved, hit count = 1 (lldb) br dis 1 1 breakpoints disabled. (lldb) br li Current breakpoints: 1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1 Options: disabled 1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, unresolved, hit count = 1 (lldb) br del 1 1 breakpoints deleted; 0 breakpoint locations disabled. (lldb) br li No breakpoints currently set.
創建斷點
在上面的例子中,我們通過在源碼頁面器的滾槽 16
上點擊來創建斷點。你可以通過把斷點拖拽出滾槽,然后釋放鼠標來刪除斷點 (消失時會有一個非常可愛的噗的一下的動畫)。你也可以在斷點導航頁選擇斷點,然后按下刪除鍵刪除。
要在調試器中創建斷點,可以使用 breakpoint set
命令。
(lldb) breakpoint set -f main.m -l 16 Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab
也可以使用縮寫形式 br
。雖然 b
是一個完全不同的命令 (_regexp-break
的縮寫),但恰好也可以實現和上面同樣的效果。
(lldb) b main.m:17 Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x000000010a3f6cc4
也可以在一個符號 (C 語言函數) 上創建斷點,而完全不用指定哪一行
(lldb) b isEven
Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00 (lldb) br s -F isEven Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
這些斷點會准確的停止在函數的開始。Objective-C 的方法也完全可以:
(lldb) breakpoint set -F "-[NSArray objectAtIndex:]" Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950 (lldb) b -[NSArray objectAtIndex:] Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950 (lldb) breakpoint set -F "+[NSSet setWithObject:]" Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820 (lldb) b +[NSSet setWithObject:] Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
如果想在 Xcode 的UI上創建符號斷點,你可以點擊斷點欄左側的 +
按鈕。
然后選擇第三個選項:
這時會出現一個彈出框,你可以在里面添加例如 -[NSArray objectAtIndex:]
這樣的符號斷點。這樣每次調用這個函數的時候,程序都會停止,不管是你調用還是蘋果調用。
如果你 Xcode 的 UI 上右擊任意斷點,然后選擇 "Edit Breakpoint" 的話,會有一些非常誘人的選擇。
這里,斷點已經被修改為只有當 i
是 99
的時候才會停止。你也可以使用 "ignore" 選項來告訴斷點最初的 n
次調用 (並且條件為真的時候) 的時候不要停止。
接下來介紹 'Add Action' 按鈕...
斷點行為 (Action)
上面的例子中,你或許想知道每一次到達斷點的時候 i
的值。我們可以使用 p i
作為斷點行為。這樣每次到達斷點的時候,都會自動運行這個命令。
你也可以添加多個行為,可以是調試器命令,shell 命令,也可以是更直接的打印:
可以看到它打印 i
,然后大聲念出那個句子,接着打印了自定義的表達式。
下面是在 LLDB 而不是 Xcode 的 UI 中做這些的時候,看起來的樣子。
(lldb) breakpoint set -F isEven Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00 (lldb) breakpoint modify -c 'i == 99' 1 (lldb) breakpoint command add 1 Enter your debugger command(s). Type 'DONE' to end. > p i > DONE (lldb) br li 1 1: name = 'isEven', locations = 1, resolved = 1, hit count = 0 Breakpoint commands: p i Condition: i == 99 1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0
接下來說說自動化。
賦值后繼續運行
看編輯斷點彈出窗口的底部,你還會看到一個選項: "Automatically continue after evaluation actions." 。它僅僅是一個選擇框,但是卻很強大。選中它,調試器會運行你所有的命令,然后繼續運行。看起來就像沒有執行任何斷點一樣 (除非斷點太多,運行需要一段時間,拖慢了你的程序)。
這個選項框的效果和讓最后斷點的最后一個行為是 continue
一樣。選框只是讓這個操作變得更簡單。調試器的輸出是:
(lldb) breakpoint set -F isEven Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00 (lldb) breakpoint command add 1 Enter your debugger command(s). Type 'DONE' to end. > continue > DONE (lldb) br li 1 1: name = 'isEven', locations = 1, resolved = 1, hit count = 0 Breakpoint commands: continue 1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0
執行斷點后自動繼續運行,允許你完全通過斷點來修改程序!你可以在某一行停止,運行一個 expression
命令來改變變量,然后繼續運行。
例子
想想所謂的"打印調試"技術吧,不要這么做:
NSLog(@"%@", whatIsInsideThisThing);
而是用個打印變量的斷點替換 log 語句,然后繼續運行。
也不要:
int calculateTheTrickyValue { return 9; /* Figure this out later. ... }
而是加一個使用 thread return 9
命令的斷點,然后讓它繼續運行。
符號斷點加上 action 真的很強大。你也可以在你朋友的 Xcode 工程上添加一些斷點,並且加上大聲朗讀某些東西的 action。看看他們要花多久才能弄明白發生了什么。😄
完全在調試器內運行
在開始舞蹈之前,還有一件事要看一看。實際上你可以在調試器中執行任何 C/Objective-C/C++/Swift 的命令。唯一的缺點就是不能創建新函數... 這意味着不能創建新的類,block,函數,有虛擬函數的 C++ 類等等。除此之外,它都可以做。
我們可以申請分配一些字節:
(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
命令),來看看新數組中的四個字節:
(lldb) x/4c $str 0x7fd04a900040: monk
我們也可以去掉 3 個字節 (x
命令需要斜引號,因為它只有一個內存地址的參數,而不是表達式;使用 help x
來獲得更多信息):
(lldb) x/1w `$str + 3` 0x7fd04a900043: keys
做完了之后,一定不要忘了釋放內存,這樣才不會內存泄露。(哈,雖然這是調試器用到的內存):
(lldb) e (void)free($str)
讓我們起舞
現在我們已經知道基本的步調了,是時候開始跳舞並玩一些瘋狂的事情了。我曾經寫過一篇 NSArray
深度探究的博客。這篇博客用了很多 NSLog
語句,但實際上我的所有探索都是在調試器中完成的。看看你能不能弄明白怎么做的,這會是一個有意思的練習。
不用斷點調試
程序運行時,Xcode 的調試條上會出現暫停按鈕,而不是繼續按鈕:
點擊按鈕會暫停 app (這會運行 process interrupt
命令,因為 LLDB 總是在背后運行)。這會讓你可以訪問調試器,但看起來可以做的事情不多,因為在當前作用域沒有變量,也沒有特定的代碼讓你看。
這就是有意思的地方。如果你正在運行 iOS app,你可以試試這個: (因為全局變量是可訪問的)
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription] <UIWindow: 0x7f82b1fa8140; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x7f82b1fa92d0>; layer = <UIWindowLayer: 0x7f82b1fa8400>> | <UIView: 0x7f82b1d01fd0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7f82b1e2e0a0>>
你可以看到整個層次。Chisel 中 pviews
就是這么實現的。
更新UI
有了上面的輸出,我們可以獲取這個 view:
(lldb) e id $myView = (id)0x7f82b1d01fd0
然后在調試器中改變它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
但是只有程序繼續運行之后才會看到界面的變化。因為改變的內容必須被發送到渲染服務中,然后顯示才會被更新。
渲染服務實際上是一個另外的進程 (被稱作 backboardd
)。這就是說即使我們正在調試的內容所在的進程被打斷了,backboardd
也還是繼續運行着的。
這意味着你可以運行下面的命令,而不用繼續運行程序:
(lldb) e (void)[CATransaction flush]
即使你仍然在調試器中,UI 也會在模擬器或者真機上實時更新。Chisel 為此提供了一個別名叫做 caflush
,這個命令被用來實現其他的快捷命令,例如 hide <view>
,show <view>
以及其他很多命令。所有 Chisel的命令都有文檔,所以安裝后隨意運行 help show
來看更多信息。
Push 一個 View Controller
想象一個以 UINavigationController
為 root ViewController 的應用。你可以通過下面的命令,輕松地獲取它:
(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]
然后 push 一個 child view controller:
(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]
最后運行下面的命令:
(lldb) caflush // e (void)[CATransaction flush]
navigation Controller 就會立刻就被 push 到你眼前。
查找按鈕的 target
想象你在調試器中有一個 $myButton
的變量,可以是創建出來的,也可以是從 UI 上抓取出來的,或者是你停止在斷點時的一個局部變量。你想知道,按鈕按下的時候誰會接收到按鈕發出的 action。非常簡單:
(lldb) po [$myButton allTargets] {( <MagicEventListener: 0x7fb58bd2e240> )} (lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0] <__NSArrayM 0x7fb58bd2aa40>( _handleTap: )
現在你或許想在它發生的時候加一個斷點。在 -[MagicEventListener _handleTap:]
設置一個符號斷點就可以了,在 Xcode 和 LLDB 中都可以,然后你就可以點擊按鈕並停在你所希望的地方了。
觀察實例變量的變化
假設你有一個 UIView
,不知道為什么它的 _layer
實例變量被重寫了 (糟糕)。因為有可能並不涉及到方法,我們不能使用符號斷點。相反的,我們想監視什么時候這個地址被寫入。
首先,我們需要找到 _layer
這個變量在對象上的相對位置:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer")) (ptrdiff_t) $0 = 8
現在我們知道 ($myView + 8)
是被寫入的內存地址:
(lldb) watchpoint set expression -- (int *)$myView + 8 Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w new value: 0x0000000000000000
這被以 wivar $myView _layer
加入到 Chisel 中。
非重寫方法的符號斷點
假設你想知道 -[MyViewController viewDidAppear:]
什么時候被調用。如果這個方法並沒有在MyViewController
中實現,而是在其父類中實現的,該怎么辦呢?試着設置一個斷點,會出現以下結果:
(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
因為 LLDB 會查找一個符號,但是實際在這個類上卻找不到,所以斷點也永遠不會觸發。你需要做的是為斷點設置一個條件 [self isKindOfClass:[MyViewController class]]
,然后把斷點放在 UIViewController
上。正常情況下這樣設置一個條件可以正常工作。但是這里不會,因為我們沒有父類的實現。
viewDidAppear:
是蘋果實現的方法,因此沒有它的符號;在方法內沒有 self
。如果想在符號斷點上使用 self
,你必須知道它在哪里 (它可能在寄存器上,也可能在棧上;在 x86 上,你可以在 $esp+4
找到它)。但是這是很痛苦的,因為現在你必須至少知道四種體系結構 (x86,x86-64,armv7,armv64)。想象你需要花多少時間去學習命令集以及它們每一個的調用約定,然后正確的寫一個在你的超類上設置斷點並且條件正確的命令。幸運的是,這個在 Chisel 被解決了。這被成為 bmessage
:
(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
LLDB 和 Python
LLDB 有內建的,完整的 Python 支持。在LLDB中輸入 script
,會打開一個 Python REPL。你也可以輸入一行 python 語句作為 script 命令
的參數,這可以運行 python 語句而不進入REPL:
(lldb) script import os (lldb) script os.system("open http://www.objc.io/")
這樣就允許你創造各種酷的命令。把下面的語句放到文件 ~/myCommands.py
中:
def caflushCommand(debugger, command, result, internal_dict): debugger.HandleCommand("e (void)[CATransaction flush]")
然后再 LLDB 中運行:
command script import ~/myCommands.py
或者把這行命令放在 /.lldbinit
里,這樣每次進入 LLDB 時都會自動運行。Chisel 其實就是一個 Python 腳本的集合,這些腳本拼接 (命令) 字符串 ,然后讓 LLDB 執行。很簡單,不是嗎?
緊握調試器這一武器
LLDB 可以做的事情很多。大多數人習慣於使用 p
,po
,n
,s
和 c
,但實際上除此之外,LLDB 可以做的還有很多。掌握所有的命令 (實際上並不是很多),會讓你在揭示代碼運行時的運行狀態,尋找 bug,強制執行特定的運行路徑時獲得更大的能力。你甚至可以構建簡單的交互原型 - 比如要是現在以 modal 方式彈出一個 View Controller 會怎么樣?使用調試器,一試便知。
這篇文章是為了想你展示 LLDB 的強大之處,並且鼓勵你多去探索在控制台輸入命令。
打開 LLDB,輸入 help
,看一看列舉的命令。你嘗試過多少?用了多少?
但願 NSLog
看起來不再那么吸引你去用,每次編輯再運行並不有趣而且耗時。
調試愉快!