LLDB是Xcode默認的調試器,它與LLVM編譯器一起,帶給我們更豐富的流程控制和數據檢測的調試功能。平時用Xcode運行程序,實際走的都是LLDB。熟練使用LLDB,可以讓你debug事半功倍。
LLDB基礎知識
LLDB控制檯
Xcode中內嵌了LLDB控制檯,在Xcode中代碼的下方,我們可以看到LLDB控制檯。 LLDB控制檯平時會輸出一些log信息。如果我們想輸入命令調試,必須讓程序進入暫停狀態。讓程序進入暫停狀態的方式主要有2種:
- 斷點或者watchpoint: 在代碼中設置一個斷點(watchpoint),當程序運行到斷點位置的時候,會進入stop狀態
- 直接暫停,控制檯上方有一個暫停按鈕,上圖紅框已標出,點擊即可暫停程序
LLDB語法
在使用LLDB之前,我們來先看看LLDB的語法,瞭解語法可以幫助我們清晰的使用LLDB:
1 | <command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]] |
一眼看上去可能比較迷茫,給大家解釋一下:
<command>
(命令)和<subcommand>
(子命令):LLDB調試命令的名稱。命令和子命令按層級結構來排列:一個命令對象爲跟隨其的子命令對象創建一個上下文,子命令又爲其子命令創建一個上下文,依此類推。<action>
:執行命令的操作<options>
:命令選項<arguement>
:命令的參數[]
:表示命令是可選的,可以有也可以沒有
舉個例子,假設我們給main方法設置一個斷點,我們使用下面的命令:
1
|
breakpoint
set
-n
main
|
這個命令對應到上面的語法就是:
command
:breakpoint
表示斷點命令action
:set
表示設置斷點option
:-n
表示根據方法name設置斷點arguement
:mian
表示方法名爲mian
原始(raw)命令
LLDB支持不帶命令選項(options)的原始(raw)命令,原始命令會將命令後面的所有東西當做參數(arguement)傳遞。不過很多原始命令也可以帶命令選項,當你使用命令選項的時候,需要在命令選項後面加--
區分命令選項和參數。
e.g: 常用的expression
就是raw命令,一般情況下我們使用expression
打印一個東西是這樣的:
1 2 | (lldb) expression count (int) $2 = 4 |
當我們想打印一個對象的時候。需要使用-O
命令選項,我們應該用--
將命令選項和參數區分:
1
2
|
(lldb)
expression
-O
--
self
<ViewController:
0x7f9000f17660>
|
唯一匹配原則
LLDB的命令遵循唯一匹配原則:假如根據前n個字母已經能唯一匹配到某個命令,則只寫前n個字母等效於寫下完整的命令。
e.g: 前面提到我設置斷點的命令,我們可以使用唯一匹配原則簡寫,下面2條命令等效:
1 2 | breakpoint set -n main br s -n main |
~/.lldbinit
LLDB有了一個啓動時加載的文件~/.lldbinit
,每次啓動都會加載。所以一些初始化的事兒,我們可以寫入~/.lldbinit
中,比如給命令定義別名等。但是由於這時候程序還沒有真正運行,也有部分操作無法在裏面玩,比如設置斷點。
LLDB命令
expression
expression命令的作用是執行一個表達式,並將表達式返回的結果輸出。expression的完整語法是這樣的:
1
|
expression
<cmd-options>
--
<expr>
|
<cmd-options>
:命令選項,一般情況下使用默認的即可,不需要特別標明。--
: 命令選項結束符,表示所有的命令選項已經設置完畢,如果沒有命令選項,--
可以省略<expr>
: 要執行的表達式
說expression
是LLDB裏面最重要的命令都不爲過。因爲他能實現2個功能。
- 執行某個表達式。 我們在代碼運行過程中,可以通過執行某個表達式來動態改變程序運行的軌跡。 假如我們在運行過程中,突然想把self.view顏色改成紅色,看看效果。我們不必寫下代碼,重新run,只需暫停程序,用
expression
改變顏色,再刷新一下界面,就能看到效果
1234// 改變顏色(lldb) expression -- self.view.backgroundColor = [UIColor redColor]// 刷新界面(lldb) expression -- (void)[CATransaction flush] - 將返回值輸出。 也就是說我們可以通過
expression
來打印東西。 假如我們想打印self.view:
1
2
|
(lldb)
expression
--
self.view
(UIView
*)
$1
=
0x00007fe322c18a10
|
p & print & call
一般情況下,我們直接用expression還是用得比較少的,更多時候我們用的是p
、print
、call
。這三個命令其實都是expression
--
的別名(--
表示不再接受命令選項,詳情見前面原始(raw)命令
這一節)
print
: 打印某個東西,可以是變量和表達式p
: 可以看做是print
的簡寫call
: 調用某個方法。
表面上看起來他們可能有不一樣的地方,實際都是執行某個表達式(變量也當做表達式),將執行的結果輸出到控制檯上。所以你可以用p
調用某個方法,也可以用call
打印東西
e.g: 下面代碼效果相同:
1 2 3 4 5 6 7 8 9 10 | (lldb) expression -- self.view (UIView *) $5 = 0x00007fb2a40344a0 (lldb) p self.view (UIView *) $6 = 0x00007fb2a40344a0 (lldb) print self.view (UIView *) $7 = 0x00007fb2a40344a0 (lldb) call self.view (UIView *) $8 = 0x00007fb2a40344a0 (lldb) e self.view (UIView *) $9 = 0x00007fb2a40344a0 |
根據唯一匹配原則,如果你沒有自己添加特殊的命令別名。
e
也可以表示expression
的意思。原始命令默認沒有命令選項,所以e
也能帶給你同樣的效果
po
我們知道,OC裏所有的對象都是用指針表示的,所以一般打印的時候,打印出來的是對象的指針,而不是對象本身。如果我們想打印對象。我們需要使用命令選項:-O
。爲了更方便的使用,LLDB爲expression -O --
定義了一個別名:po
1
2
3
4
5
6
|
(lldb)
expression
--
self.view
(UIView
*)
$13
=
0x00007fb2a40344a0
(lldb)
expression
-O
--
self.view
<UIView:
0x7fb2a40344a0;
frame
=
(0
0;
375
667);
autoresize
=
W+H;
layer
=
<CALayer:
0x7fb2a4018c80>>
(lldb)
po
self.view
<UIView:
0x7fb2a40344a0;
frame
=
(0
0;
375
667);
autoresize
=
W+H;
layer
=
<CALayer:
0x7fb2a4018c80>>
|
還有其他很多命令選項,不過我們一般用得比較少,所以我就不具體的一一介紹了,如果想了解,在LLDB控制檯上輸入:
help expression
即可查到expression所有的信息
thread
thread backtrace
& bt
有時候我們想要了解線程堆棧信息,可以使用thread backtrace
thread
backtrace
作用是將線程的堆棧打印出來。我們來看看他的語法
1 | thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>] |
thread backtrace
後面跟的都是命令選項:
-c
:設置打印堆棧的幀數(frame)-s
:設置從哪個幀(frame)開始打印-e
:是否顯示額外的回溯
實際上這些命令選項我們一般不需要使用。
e.g: 當發生crash的時候,我們可以使用thread backtrace
查看堆棧調用
1
2
3
4
5
6
7
8
9
|
(lldb)
thread
backtrace
*
thread
#1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylib`objc_msgSend
+ 11, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame
#0: 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11
*
frame
#1: 0x000000010aa9f75e TLLDB`-[ViewController viewDidLoad](self=0x00007fa270e1f440,
_cmd="viewDidLoad") + 174 at ViewController.m:23
frame
#2: 0x000000010ba67f98 UIKit`-[UIViewController loadViewIfRequired]
+ 1198
frame
#3: 0x000000010ba682e7 UIKit`-[UIViewController view] +
27
frame
#4: 0x000000010b93eab0 UIKit`-[UIWindow addRootViewControllerViewIfPossible]
+ 61
frame
#5: 0x000000010b93f199 UIKit`-[UIWindow _setHidden:forced:]
+ 282
frame
#6: 0x000000010b950c2e UIKit`-[UIWindow makeKeyAndVisible]
+ 42
|
我們可以看到crash發生在-[ViewController viewDidLoad]
中的第23行,只需檢查這行代碼是不是幹了什麼非法的事兒就可以了。
LLDB還爲backtrace專門定義了一個別名:bt
,他的效果與thread
backtrace
相同,如果你不想寫那麼長一串字母,直接寫下bt
即可:
1 | (lldb) bt |
thread return
Debug的時候,也許會因爲各種原因,我們不想讓代碼執行某個方法,或者要直接返回一個想要的值。這時候就該thread return
上場了。
1
|
thread
return
[<expr>]
|
thread return
可以接受一個表達式,調用命令之後直接從當前的frame返回表達式的值。
e.g: 我們有一個someMethod
方法,默認情況下是返回YES。我們想要讓他返回NO
我們只需在方法的開始位置加一個斷點,當程序中斷的時候,輸入命令即可:
1 | (lldb) thread return NO |
效果相當於在斷點位置直接調用return NO;
,不會執行斷點後面的代碼
c & n & s & finish
一般在調試程序的時候,我們經常用到下面這4個按鈕:
用觸摸板的孩子們可能會覺得點擊這4個按鈕比較費勁。其實LLDB命令也可以完成上面的操作,而且如果不輸入命令,直接按Enter鍵,LLDB會自動執行上次的命令。按一下Enter就能達到我們想要的效果,有木有頓時感覺逼格滿滿的!!! 我們來看看對應這4個按鈕的LLDB命令:
c
/continue
/thread continue
: 這三個命令效果都等同於上圖中第一個按鈕的。表示程序繼續運行n
/next
/thread step-over
: 這三個命令效果等同於上圖第二個按鈕。表示單步運行s
/step
/thread step-in
: 這三個命令效果等同於上圖第三個按鈕。表示進入某個方法finish
/step-out
: 這兩個命令效果等同於第四個按鈕。表示直接走完當前方法,返回到上層frame
thread其他不常用的命令
thread 相關的還有其他一些不常用的命令,這裏就簡單介紹一下即可,如果需要了解更多,可以使用命令help thread
查閱
thread jump
: 直接讓程序跳到某一行。由於ARC下編譯器實際插入了不少retain,release命令。跳過一些代碼不執行很可能會造成對象內存混亂髮生crash。thread list
: 列出所有的線程thread select
: 選擇某個線程thread until
: 傳入一個line的參數,讓程序執行到這行的時候暫停thread info
: 輸出當前線程的信息
frame
前面我們提到過很多次frame(幀)。可能有的朋友對frame這個概念還不太瞭解。隨便打個斷點
我們在控制檯上輸入命令bt
,可以打印出來所有的frame。如果仔細觀察,這些frame和左邊紅框裏的堆棧是一致的。平時我們看到的左邊的堆棧就是frame。
frame variable
平時Debug的時候我們經常做的事就是查看變量的值,通過frame variable
命令,可以打印出當前frame的所有變量
1
2
3
4
5
|
(lldb)
frame
variable
(ViewController
*)
self
=
0x00007fa158526e60
(SEL)
_cmd
=
"text:"
(BOOL)
ret
=
YES
(int)
a
=
3
|
可以看到,他將self
,_cmd
,ret
,a
等本地變量都打印了出來
如果我們要需要打印指定變量,也可以給frame variable
傳入參數:
1 2 | (lldb) frame variable self->_string (NSString *) self->_string = nil |
不過frame variable
只接受變量作爲參數,不接受表達式,也就是說我們無法使用frame variable self.string
,因爲self.string
是調用string
的getter
方法。所以一般打印指定變量,我更喜歡用p
或者po
。
其他不常用命令
一般frame variable
打印所有變量用得比較多,frame還有2個不怎麼常用的命令:
frame info
: 查看當前frame的信息
1
2
|
(lldb)
frame
info
frame
#0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60,
_cmd="text:", ret=YES) + 37 at ViewController.m:38
|
frame select
: 選擇某個frame
1 2 3 4 5 6 7 8 9 | (lldb) frame select 1 frame #1: 0x0000000101bf872e TLLDB`-[ViewController viewDidLoad](self=0x00007fa158526e60, _cmd="viewDidLoad") + 78 at ViewController.m:23 20 21 - (void)viewDidLoad { 22 [super viewDidLoad]; -> 23 [self text:YES]; 24 NSLog(@"1"); 25 NSLog(@"2"); 26 NSLog(@"3"); |
當我們選擇frame 1的時候,他會把frame1的信息和代碼打印出來。不過一般我都是直接在Xcode左邊點擊某個frame,這樣更方便
breakpoint
調試過程中,我們用得最多的可能就是斷點了。LLDB中的斷點命令也非常強大
breakpoint set
breakpoint set
命令用於設置斷點,LLDB提供了很多種設置斷點的方式:
使用-n
根據方法名設置斷點:
e.g: 我們想給所有類中的viewWillAppear:
設置一個斷點:
1
2
|
(lldb)
breakpoint
set
-n
viewWillAppear:
Breakpoint
13:
33
locations.
|
使用-f
指定文件
e.g: 我們只需要給ViewController.m
文件中的viewDidLoad
設置斷點:
1 2 | (lldb) breakpoint set -f ViewController.m -n viewDidLoad Breakpoint 22: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:22, address = 0x000000010272a6f4 |
這裏需要注意,如果方法未寫在文件中(比如寫在category文件中,或者父類文件中),指定文件之後,將無法給這個方法設置斷點。
使用-l
指定文件某一行設置斷點
e.g: 我們想給ViewController.m
第38行設置斷點
1
2
|
(lldb)
breakpoint
set
-f
ViewController.m
-l
38
Breakpoint
23:
where
=
TLLDB`-[ViewController
text:]
+
37
at
ViewController.m:38,
address
=
0x000000010272a7d5
|
使用-c
設置條件斷點
e.g: text:
方法接受一個ret
的參數,我們想讓ret
== YES
的時候程序中斷:
1 2 | (lldb) breakpoint set -n text: -c ret == YES Breakpoint 7: where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x0000000105ef37ce |
使用-o
設置單次斷點
e.g: 如果剛剛那個斷點我們只想讓他中斷一次:
1
2
|
(lldb)
breakpoint
set
-n
text:
-o
'breakpoint 3':
where
=
TLLDB`-[ViewController
text:]
+
30
at
ViewController.m:37,
address
=
0x000000010b6f97ce
|
breakpoint command
有的時候我們可能需要給斷點添加一些命令,比如每次走到這個斷點的時候,我們都需要打印self
對象。我們只需要給斷點添加一個po
self
命令,就不用每次執行斷點再自己輸入po self
了
breakpoint command add
breakpoint command add
命令就是給斷點添加命令的命令。
e.g: 假設我們需要在ViewController
的viewDidLoad
中查看self.view
的值
我們首先給-[ViewController viewDidLoad]
添加一個斷點
1 2 | (lldb) breakpoint set -n "-[ViewController viewDidLoad]" 'breakpoint 3': where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:23, address = 0x00000001055e6004 |
可以看到添加成功之後,這個breakpoint
的id爲3,然後我們給他增加一個命令:po self.view
1
|
(lldb)
breakpoint
command
add
-o
"po self.view"
3
|
-o
完整寫法是--one-liner
,表示增加一條命令。3
表示對id爲3
的breakpoint
增加命令。
添加完命令之後,每次程序執行到這個斷點就可以自動打印出self.view
的值了
如果我們一下子想增加多條命令,比如我想在viewDidLoad
中打印當前frame的所有變量,但是我們不想讓他中斷,也就是在打印完成之後,需要繼續執行。我們可以這樣玩:
1 2 3 4 5 | (lldb) breakpoint command add 3 Enter your debugger command(s). Type 'DONE' to end. > frame variable > continue > DONE |
輸入breakpoint command add 3
對斷點3增加命令。他會讓你輸入增加哪些命令,輸入’DONE’表示結束。這時候你就可以輸入多條命令了
多次對同一個斷點添加命令,後面命令會將前面命令覆蓋
breakpoint command list
如果想查看某個斷點已有的命令,可以使用breakpoint command list
。 e.g: 我們查看一下剛剛的斷點3已有的命令
1
2
3
4
5
|
(lldb)
breakpoint
command
list
3
'breakpoint 3':
Breakpoint
commands:
frame
variable
continue
|
可以看到一共有2條命令,分別爲frame variable
和continue
breakpoint command delete
有增加就有刪除,breakpoint command delete
可以讓我們刪除某個斷點的命令 e.g: 我們將斷點3中的命令刪除:
1 2 3 | (lldb) breakpoint command delete 3 (lldb) breakpoint command list 3 Breakpoint 3 does not have an associated command. |
可以看到刪除之後,斷點3就沒有命令了
breakpoint list
如果我們想查看已經設置了哪些斷點,可以使用breakpoint list
e.g:
1
2
3
4
|
(lldb)
breakpoint
list
Current
breakpoints:
4:
name
=
'-[ViewController viewDidLoad]',
locations
=
1,
resolved
=
1,
hit
count
=
0
4.1:
where
=
TLLDB`-[ViewController
viewDidLoad]
+
20
at
ViewController.m:23,
address
=
0x00000001055e6004,
resolved,
hit
count
=
0
|
我們可以看到當前只有一個斷點,打在-[ViewController viewDidLoad]
上,id是4
breakpoint disable/enable
有的時候我們可能暫時不想要某個斷點,可以使用breakpoint disable
讓某個斷點暫時失效 e.g: 我們來讓剛剛的斷點4失效
1 2 | (lldb) breakpoint disable 4 1 breakpoints disabled. |
輸入完命令之後,顯示斷點已經失效
當我們又需要這個斷點的時候,可以使用breakpoint enable
再次讓他生效 e.g: 重新啓用斷點4
1
2
|
(lldb)
breakpoint
enable
4
1
breakpoints
enabled.
|
breakpoint delete
如果我們覺得這個斷點以後再也用不上了,可以用breakpoint delete
直接刪除斷點. e.g: 刪除斷點4
1 2 | (lldb) breakpoint delete 4 1 breakpoints deleted; 0 breakpoint locations disabled. |
如果我們想刪除所有斷點,只需要不指定breakpoint delete
參數即可
1
2
3
|
(lldb)
breakpoint
delete
About
to
delete
all
breakpoints,
do
you
want
to
do
that?:
[Y/n]
y
All
breakpoints
removed.
(1
breakpoint)
|
刪除的時候他會提示你,是不是真的想刪除所有斷點,需要你再次輸入Y
確認。如果想直接刪除,不需要他的提示,使用-f
命令選項即可
1 2 | (lldb) breakpoint delete -f All breakpoints removed. (1 breakpoint) |
實際平時我們真正使用
breakpoint
命令反而比較少,因爲Xcode已經內置了斷點工具。我們可以直接在代碼上打斷點,可以在斷點工具欄裏面查看編輯斷點,這比使用LLDB命令方便很多。不過了解LLDB相關命令可以讓我們對斷點理解更深刻。 如果你想了解怎麼使用Xcode設置斷點,可以閱讀這篇文章《Xcode中斷點的威力》
watchpoint
breakpoint
有一個孿生兄弟watchpoint
。如果說breakpoint
是對方法生效的斷點,watchpoint
就是對地址生效的斷點
如果我們想要知道某個屬性什麼時候被篡改了,我們該怎麼辦呢?有人可能會說對setter方法打個斷點不就行了麼?但是如果更改的時候沒調用setter方法呢? 這時候最好的辦法就是用watchpoint
。我們可以用他觀察這個屬性的地址。如果地址裏面的東西改變了,就讓程序中斷
watchpoint set
watchpoint set
命令用於添加一個watchpoint
。只要這個地址中的內容變化了,程序就會中斷。
watchpoint set variable
一般情況下,要觀察變量或者屬性,使用watchpoint set variable
命令即可 e.g: 觀察self->_string
1
2
3
4
|
(lldb)
watchpoint
set
variable
self->_string
Watchpoint
created:
Watchpoint
1:
addr
=
0x7fcf3959c418
size
=
8
state
=
enabled
type
=
w
watchpoint
spec
=
'self->_string'
new
value:
0x0000000000000000
|
watchpoint set variable
傳入的是變量名。需要注意的是,這裏不接受方法,所以不能使用watchpoint
set variable self.string
,因爲self.string調用的是string的getter方法
watchpoint set expression
如果我們想直接觀察某個地址,可以使用watchpoint set expression
e.g: 我們先拿到_model的地址,然後對地址設置一個watchpoint
1 2 3 4 5 | (lldb) p &_model (Modek **) $3 = 0x00007fe0dbf23280 (lldb) watchpoint set expression 0x00007fe0dbf23280 Watchpoint created: Watchpoint 1: addr = 0x7fe0dbf23280 size = 8 state = enabled type = w new value: 0 |
watchpoint command
跟breakpoint類似,在watchpoint中也可以添加命令
watchpoint command add
我們來看看怎麼給watchpoint
添加命令:
首先,我們設置一個watchpoint:
1
2
3
4
|
(lldb)
watchpoint
set
variable
_string
Watchpoint
created:
Watchpoint
1:
addr
=
0x7fe4e1444760
size
=
8
state
=
enabled
type
=
w
watchpoint
spec
=
'_string'
new
value:
0x0000000000000000
|
可以看到這個watchpoint的id是1。我們可以用watchpoint command add -o
添加單條命令
1 | watchpoint command add -o 'bt' 1 |
我們在watchpoint停下來的時候,打印了他的線程信息。
我們也可以一次添加多條命令:
1
2
3
4
5
|
(lldb)
watchpoint
command
add
1
Enter
your
debugger
command(s). Type
'DONE'
to
end.
>
bt
>
continue
>
DONE
|
可以看到watchpoint
的使用方法跟breakpoint
幾乎一模一樣。
watchpoint command list
我們可以用watchpoint command list
列出某個watchpoint
所有的command
1 2 3 4 5 | (lldb) watchpoint command list 1 Watchpoint 1: watchpoint commands: bt continue |
watchpoint command delete
我們也可以用watchpoint command delete
刪除某個watchpoint
所有的command
1
2
3
|
(lldb)
watchpoint
command
delete
1
(lldb)
watchpoint
command
list
1
Watchpoint
1
does
not
have
an
associated
command.
|
watchpoint list
如果我們想看當前所有watchpoint,可以使用watchpoint list
:
1 2 3 4 5 6 7 | (lldb) watchpoint list Number of supported hardware watchpoints: 4 Current watchpoints: Watchpoint 1: addr = 0x7fe9f9f28e30 size = 8 state = enabled type = w watchpoint spec = '_string' old value: 0x0000000000000000 new value: 0x000000010128e0d0 |
可以看到,只有一個watchpoint。
watchpoint disable
當我們不想讓某個watchpoint生效的時候,可以用watchpoint disable
:
1
2
|
(lldb)
watchpoint
disable
1
1
watchpoints
disabled.
|
再次查看這個watchpoint,可以看到他的state已經變爲了disabled
1 2 3 4 5 6 7 | (lldb) watchpoint list Number of supported hardware watchpoints: 4 Current watchpoints: Watchpoint 1: addr = 0x7fe9f9f28e30 size = 8 state = disabled type = w watchpoint spec = '_string' old value: 0x0000000000000000 new value: 0x000000010128e0d0 |
watchpoint enable
過了一會,我們又要用這個watchpoint
了,這時候可以使用watchpoint enable
:
1
2
|
(lldb)
watchpoint
enable
1
1
watchpoints
enabled.
|
watchpoint delete
如果我們覺得再也用不着這個watchpoint
了,可以用watchpoint
delete
將他刪除:
1 2 3 4 5 | (lldb) watchpoint delete 1 1 watchpoints deleted. (lldb) watchpoint list Number of supported hardware watchpoints: 4 No watchpoints currently set. |
刪除之後,我們可以看到watchpoint list
裏面已經沒有watchpoint1
了
如果有很多個watchpoint,我們想全都幹掉,只需要不指定具體哪個watchpoint即可:
1
2
3
|
(lldb)
watchpoint
delete
About
to
delete
all
watchpoints,
do
you
want
to
do
that?:
[Y/n]
y
All
watchpoints
removed.
(2
watchpoints)
|
target
target modules lookup(image lookup)
對於target這個命令,我們用得最多的可能就是target modules lookup
。由於LLDB給target
modules
取了個別名image
,所以這個命令我們又可以寫成image
lookup
。
image lookup –address
當我們有一個地址,想查找這個地址具體對應的文件位置,可以使用image lookup --address
,簡寫爲image
lookup -a
e.g: 當我們發生一個crash
1 2 3 4 5 6 7 8 9 | 2015-12-17 14:51:06.301 TLLDB[25086:246169] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 1 beyond bounds for empty NSArray' *** First throw call stack: ( 0 CoreFoundation 0x000000010accde65 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x000000010a746deb objc_exception_throw + 48 2 CoreFoundation 0x000000010ac7c395 -[__NSArray0 objectAtIndex:] + 101 3 TLLDB 0x000000010a1c3e36 -[ViewController viewDidLoad] + 86 4 UIKit 0x000000010b210f98 -[UIViewController loadViewIfRequired] + 1198 5 UIKit 0x000000010b2112e7 -[UIViewController view] + 27 |
我們可以看到是由於-[__NSArray0 objectAtIndex:]:
超出邊界而導致的crash,但是objectAtIndex:
的代碼到底在哪兒呢?
1
2
3
|
(lldb)
image
lookup
-a
0x000000010a1c3e36
Address:
TLLDB[0x0000000100000e36]
(TLLDB.__TEXT.__text
+
246)
Summary:
TLLDB`-[ViewController
viewDidLoad]
+
86
at
ViewController.m:32
|
根據0x000000010a1c3e36 -[ViewController viewDidLoad]
裏面的地址,使用image
lookup --address
查找,我們可以看到代碼位置在ViewController.m
裏面的32行
image lookup –name
當我們想查找一個方法或者符號的信息,比如所在文件位置等。我們可以使用image lookup --name
,簡寫爲image
lookup -n
。
e.g: 剛剛遇到的真問題,某個第三方SDK用了一個我們項目裏原有的第三方庫,庫裏面對NSDictionary添加了category。也就是有2個class對NSDictionary添加了名字相同的category,項目中調用自己的category的地方實際走到了第三方SDK裏面去了。最大的問題是,這2個同名category方法行爲並不一致,導致出現bug
現在問題來了,怎麼尋找到底是哪個第三方SDK?方法完全包在.a裏面。
其實只需使用image lookup -n
即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | (lldb) image lookup -n dictionaryWithXMLString: 2 matches found in /Users/jiangliancheng/Library/Developer/Xcode/DerivedData/VideoIphone-aivsnqmlwjhxapdlvmdmrubbdxpq/Build/Products/Debug-iphoneos/BaiduIphoneVideo.app/BaiduIphoneVideo: Address: BaiduIphoneVideo[0x00533a7c] (BaiduIphoneVideo.__TEXT.__text + 5414908) Summary: BaiduIphoneVideo`+[NSDictionary(SAPIXmlDictionary) dictionaryWithXMLString:] at XmlDictionary.m Module: file = "/Users/jiangliancheng/Library/Developer/Xcode/DerivedData/VideoIphone-aivsnqmlwjhxapdlvmdmrubbdxpq/Build/Products/Debug-iphoneos/BaiduIphoneVideo.app/BaiduIphoneVideo", arch = "armv7" CompileUnit: id = {0x00000000}, file = "/Users/jiangliancheng/Development/Work/iOS_ShareLib/SharedLib/Srvcs/BDPassport4iOS/BDPassport4iOS/SAPI/Extensive/ThirdParty/XMLDictionary/XmlDictionary.m", language = "Objective-C" Function: id = {0x23500000756}, name = "+[NSDictionary(SAPIXmlDictionary) dictionaryWithXMLString:]", range = [0x005a6a7c-0x005a6b02) FuncType: id = {0x23500000756}, decl = XmlDictionary.m:189, clang_type = "NSDictionary *(NSString *)" Blocks: id = {0x23500000756}, range = [0x005a6a7c-0x005a6b02) LineEntry: [0x005a6a7c-0x005a6a98): /Users/jiangliancheng/Development/Work/iOS_ShareLib/SharedLib/Srvcs/BDPassport4iOS/BDPassport4iOS/SAPI/Extensive/ThirdParty/XMLDictionary/XmlDictionary.m Symbol: id = {0x0000f2d5}, range = [0x005a6a7c-0x005a6b04), name="+[NSDictionary(SAPIXmlDictionary) dictionaryWithXMLString:]" Variable: id = {0x23500000771}, name = "self", type = "Class", location = [sp+32], decl = Variable: id = {0x2350000077e}, name = "_cmd", type = "SEL", location = [sp+28], decl = Variable: id = {0x2350000078b}, name = "string", type = "NSString *", location = [sp+24], decl = XmlDictionary.m:189 Variable: id = {0x23500000799}, name = "data", type = "NSData *", location = [sp+20], decl = XmlDictionary.m:192 Address: BaiduIphoneVideo[0x012ee160] (BaiduIphoneVideo.__TEXT.__text + 19810016) Summary: BaiduIphoneVideo`+[NSDictionary(XMLDictionary) dictionaryWithXMLString:] at XMLDictionary.m Module: file = "/Users/jiangliancheng/Library/Developer/Xcode/DerivedData/VideoIphone-aivsnqmlwjhxapdlvmdmrubbdxpq/Build/Products/Debug-iphoneos/BaiduIphoneVideo.app/BaiduIphoneVideo", arch = "armv7" CompileUnit: id = {0x00000000}, file = "/Users/wingle/Workspace/qqlive4iphone/iphone_4.0_fabu_20150601/Common_Proj/mobileTAD/VIDEO/Library/Third Party/XMLDictionary/XMLDictionary.m", language = "Objective-C" Function: id = {0x79900000b02}, name = "+[NSDictionary(XMLDictionary) dictionaryWithXMLString:]", range = [0x01361160-0x0136119a) FuncType: id = {0x79900000b02}, decl = XMLDictionary.m:325, clang_type = "NSDictionary *(NSString *)" Blocks: id = {0x79900000b02}, range = [0x01361160-0x0136119a) LineEntry: [0x01361160-0x01361164): /Users/wingle/Workspace/qqlive4iphone/iphone_4.0_fabu_20150601/Common_Proj/mobileTAD/VIDEO/Library/Third Party/XMLDictionary/XMLDictionary.m Symbol: id = {0x0003a1e9}, range = [0x01361160-0x0136119c), name="+[NSDictionary(XMLDictionary) dictionaryWithXMLString:]" Variable: id = {0x79900000b1e}, name = "self", type = "Class", location = r0, decl = Variable: id = {0x79900000b2c}, name = "_cmd", type = "SEL", location = r1, decl = Variable: id = {0x79900000b3a}, name = "string", type = "NSString *", location = r2, decl = XMLDictionary.m:325 Variable: id = {0x79900000b4a}, name = "data", type = "NSData *", location = r2, decl = XMLDictionary.m:327 |
東西有點多,我們只需關注裏面的file這一行:
1
2
|
CompileUnit:
id
=
{0x00000000},
file
=
"/Users/jiangliancheng/Development/Work/iOS_ShareLib/SharedLib/Srvcs/BDPassport4iOS/BDPassport4iOS/SAPI/Extensive/ThirdParty/XMLDictionary/XmlDictionary.m",
language
=
"Objective-C"
CompileUnit:
id
=
{0x00000000},
file
=
"/Users/wingle/Workspace/qqlive4iphone/iphone_4.0_fabu_20150601/Common_Proj/mobileTAD/VIDEO/Library/Third
Party/XMLDictionary/XMLDictionary.m",
language
=
"Objective-C"
|
可以清晰的看到,LLDB給我們找出來了這個方法的位置。 當然這個命令也可以找到方法的其他相關信息,比如參數等.
image lookup –type
當我們想查看一個類型的時候,可以使用image lookup --type
,簡寫爲image
lookup -t
:
e.g: 我們來看看Model的類型:
1 2 3 4 5 6 7 8 9 10 | (lldb) image lookup -t Model Best match found in /Users/jiangliancheng/Library/Developer/Xcode/DerivedData/TLLDB-beqoowskwzbttrejseahdoaivpgq/Build/Products/Debug-iphonesimulator/TLLDB.app/TLLDB: id = {0x30000002f}, name = "Model", byte-size = 32, decl = Modek.h:11, clang_type = "@interface Model : NSObject{ NSString * _bb; NSString * _cc; NSString * _name; } @property ( getter = name,setter = setName:,readwrite,nonatomic ) NSString * name; @end " |
可以看到,LLDB把Model這個class的所有屬性和成員變量都打印了出來,當我們想了解某個類的時候,直接使用image lookup -t
即可
target stop-hook
我們知道,用LLDB debug,大多數時候需要讓程序stop,不管用breakpoint
還是用watchpoint
。
target stop-hook
命令就是讓你可以在每次stop的時候去執行一些命令
target stop-hook
只對breakpoint
和watchpoint
的程序stop生效,直接點擊Xcode上的pause
或者debug view hierarchy
不會生效
target stop-hook add & display
假如我們想在每次程序stop的時候,都用命令打印當前frame的所有變量。我們可以添加一個stop-hook:
1
2
|
(lldb)
target
stop-hook
add
-o
"frame variable"
Stop
hook
#4 added.
|
target stop-hook add
表示添加stop-hook,-o
的全稱是--one-liner
,表示添加一條命令。
我們看一下,當執行到一個斷點的時候會發生什麼?
1 2 3 4 | - Hook 1 (frame variable) (ViewController *) self = 0x00007fd55b12e380 (SEL) _cmd = "viewDidLoad" (NSMutableURLRequest *) request = 0x00007fd55b1010c0 |
在程序stop的時候,他會自動執行frame variable
,打印出了所有的變量。
大多情況下,我們在stop的時候可能想要做的是打印一個東西。正常情況我們需要用target stop-hook add -o "p xxx"
,LLDB提供了一個更簡便的命令display
。
e.g: 下面2行代碼效果相同
1
2
|
(lldb)
target
stop-hook
add
-o
"p self.view"
(lldb)
display
self.view
|
也可以用
display
來執行某一個命令。p
,e
,expression
是等效的。
target stop-hook list
當添加完stop-hook之後,我們想看當前所有的stop-hook
怎麼辦呢?使用stop-hook
list
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | (lldb) target stop-hook list Hook: 4 State: enabled Commands: frame variable Hook: 5 State: enabled Commands: expression self.view Hook: 6 State: enabled Commands: expr -- self.view |
我們可以看到,我們添加了4個stop-hook
,每個stop-hook
都有一個id,他們分別是4,5,6
target stop-hook delete & undisplay
有添加的命令,當然也就有刪除的命令。使用target stop-hook delete
可以刪除stop-hook
,如果你覺得這個命令有點長,懶得敲。你也可以用undisplay
1
2
|
(lldb)
target
stop-hook
delete
4
(lldb)
undisplay
5
|
我們用target stop-hook delete
和undisplay
分別刪除了id爲4和5的stop-hook
target stop-hook disable/enable
當我們暫時想讓某個stop-hook
失效的時候,可以使用target
stop-hook disable
1 | (lldb) target stop-hook disable 8 |
如果我們想讓所有的stop-hook
失效,只需不傳入stop-hook
id即可:
1
|
(lldb)
target
stop-hook
disable
|
有disable
就有enable
,我們又想讓stop-hook
生效了。可以使用target
stop-hook enable
1 | (lldb) target stop-hook enable 8 |
同理,不傳入參數表示讓所有stop-hook
生效
1
|
(lldb)
target
stop-hook
enable
|
Extension
前幾天@兔be南玻1在微博上給出一個小技巧。LLDB中@import UIKit
即可打印frame等變量(默認情況下打不出來)微博鏈接。
1 2 3 4 5 6 | (lldb) p self.view.frame error: property 'frame' not found on object of type 'UIView *' error: 1 errors parsing expression (lldb) e @import UIKit (lldb) p self.view.frame (CGRect) $0 = (origin = (x = 0, y = 0), size = (width = 375, height = 667)) |
由於每次run Xcode,LLDB的東西都會被清空。所以每次run你都需要在LLDB中輸入e @import UIKit
才能使用這個方便的功能,有點麻煩呀!
之後有人提出了比較方便的一個辦法。給UIApplicationMain設置一個斷點,在斷點中添加執行e @import UIKit
。 這種方法非常方便,不用自己輸入了,但是斷點我們可能會誤刪,而且斷點是對應工程的。換一個工程又得重新打一個這樣的斷點。還是有點麻煩。有沒有更簡便的方法呢?
我們首先想到的是LLDB在每次啓動的時候都會load ‘~/.lldbinit’文件。在這裏面執行e @import UIKit
不就行了麼?不會被誤刪,對每個工程都有效!
然而想法是美好的,現實卻是殘酷的!因爲UIKit
這個庫是在target中。而load ‘~/.lldbinit’的時候target還沒創建。所以無法import UIKit
。stackoverflow詳細解釋
這時候我們又想到,可不可以在’~/.lldbinit’中給UIApplicationMain設置一個斷點,在斷點中添加執行e @import UIKit
呢?
答案是不行。原因跟前面一樣,load ‘~/.lldbinit’執行時間太早。斷點是依賴target的,target還未創建,斷點加不上去。好事多磨,道路坎坷呀~~~
後來我們又想到用stop-hook
行不行呢?stop-hook
不依賴target。一般我們p frame
的時候,都需要先stop,理論上是可行的
事實證明stop-hook
的方法完全ok。只需要在’~/.lldbinit’中添加這2條命令即可:
1
2
|
display
@import
UIKit
target
stop-hook
add
-o
"target stop-hook disable"
|
- 命令1:使用
display
表示在stop的時候執行@import UIKit
- 命令2:由於我們只需要執行一次
@import UIKit
,所以執行完成之後,執行target stop-hook disable
,使原有的所有stop-hook
失效
這個命令有個缺陷,直接點擊Xcode上的
pause
和debug view hierarchy
,stop-hook
不會生效。正在探索有沒有更好的辦法完成@import UIKit
,如果你想到了,可以聯繫我~
target symbols add(add-dsym)
說這個命令之前,先簡單解釋一下dSYM文件。程序運行的時候,都會編譯成二進制文件。因爲計算機只識別二進制文件,那爲什麼我們還能在代碼上打斷點?
這主要是因爲在編譯的時候Xcode會生成dSYM文件,dSYM文件記錄了哪行代碼對應着哪些二進制,這樣我們對代碼打斷點就會對應到二進制上。dSYM詳細資料
當Xcode找不着dSYM文件的時候,我們就無法對代碼打斷點,進行調試。target symbols add
命令的作用就是讓我們可以手動的將dSYM文件添加上去。LLBD對這個命令起了一個別名: add-dsym
e.g: 當我們對接framework的時候,如果只有framework代碼,沒有工程代碼,能不能debug呢?其實我們只需要拿到工程的ipa和dSYM文件,就可以debug了,通過Attach to Process,使用命令add-dsym
將dSYM文件加入target,即可只debug
framework,不需要工程的代碼
1 | add-dsym ~/.../XXX.dSYM |
詳細細節可以查看iOS中framework的聯調
help & apropos
LLDB的命令其實還有很多,很多命令我也沒玩過。就算玩過的命令,我們也非常容易忘記,下次可能就不記得是怎麼用的了。還好LLDB給我們提供了2個查找命令的命令:help
& apropos
help
直接在LLDB中輸入help。可以查看所有的LLDB命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
(lldb)
help
Debugger
commands:
apropos
--
Find
a
list
of
debugger
commands
related
to
a
particular
word/subject.
breakpoint --
A
set
of
commands
for
operating
on
breakpoints.
Also
see
_regexp-break.
help --
Show
a
list
of
all
debugger
commands,
or
give
details
about
specific
commands.
script --
Pass
an
expression
to
the
script
interpreter
for
evaluation
and
return
the
results.
Drop
into
the
interactive
interpreter
if
no
expression
is
given.
settings --
A
set
of
commands
for
manipulating
internal
settable
debugger
variables.
source --
A
set
of
commands
for
accessing
source
file
information
target --
A
set
of
commands
for
operating
on
debugger
targets.
thread --
A
set
of
commands
for
operating
on
one
or
more
threads
within
a
running
process.
type --
A
set
of
commands
for
operating
on
the
type
system
version
--
Show
version
of
LLDB
debugger.
watchpoint --
A
set
of
commands
for
operating
on
watchpoints.
....(東西太多,只截取了一部分)
|
如果我們想看具體某一個命令的詳細用法,可以使用help <command-name>
e.g: 我們查看watchpoint
命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | (lldb) help watchpoint The following subcommands are supported: command -- A set of commands for adding, removing and examining bits of code to be executed when the watchpoint is hit (watchpoint 'commmands'). delete -- Delete the specified watchpoint(s). If no watchpoints are specified, delete them all. disable -- Disable the specified watchpoint(s) without removing it/them. If no watchpoints are specified, disable them all. enable -- Enable the specified disabled watchpoint(s). If no watchpoints are specified, enable all of them. ignore -- Set ignore count on the specified watchpoint(s). If no watchpoints are specified, set them all. list -- List all watchpoints at configurable levels of detail. modify -- Modify the options on a watchpoint or set of watchpoints in the executable. If no watchpoint is specified, act on the last created watchpoint. Passing an empty argument clears the modification. set -- A set of commands for setting a watchpoint. |
apropos
有的時候,我們可能並不能完全記得某個命令,如果只記得命令中的某個關鍵字。這時候我們可以使用apropos
搜索相關命令信息。
e.g: 我們想使用stop-hook
的命令,但是已經不記得stop-hook
命令是啥樣了
1
2
3
4
5
6
7
8
9
10
11
|
(lldb)
apropos
stop-hook
The
following
built-in
commands
may
relate
to
'stop-hook':
_regexp-display --
Add
an
expression
evaluation
stop-hook.
_regexp-undisplay --
Remove
an
expression
evaluation
stop-hook.
target
stop-hook
--
A
set
of
commands
for
operating
on
debugger
target
stop-hooks.
target
stop-hook
add
--
Add
a
hook
to
be
executed
when
the
target
stops.
target
stop-hook
delete --
Delete
a
stop-hook.
target
stop-hook
disable
--
Disable
a
stop-hook.
target
stop-hook
enable --
Enable
a
stop-hook.
target
stop-hook
list --
List
all
stop-hooks.
|
可以看到使用apropos stop-hook
搜索一下,即可將所有stop-hook
相關命令搜索出來
常用的Debug快捷鍵
debug的時候,使用快捷鍵是一個很好的習慣,我簡單列舉了幾個debug的快捷鍵
功能 | 命令 |
---|---|
暫停/繼續 | cmd + ctrl + Y |
斷點失效/生效 | cmd + Y |
控制檯顯示/隱藏 | cmd + shift + Y |
光標切換到控制檯 | cmd + shift + C |
清空控制檯 | cmd + K |
step over | F6 |
step into | F7 |
step out | F8 |
End
東西有點多,感謝大家耐心看完這篇文章。LLDB命令非常多,有很多LLDB命令我也沒玩過。這些命令我們不一定要完全記住,只要有個印象LLDB可以實現哪些功能就可以了。具體用的時候再用help
或者apropos
查找。