天天看點

Xcode進階調試技巧(1)

在蘋果的官方文檔中列出了我們在調試中能用到的一些指令,我們在這重點講一些常用的指令

調試本地檔案方法(Mac OS X):(lldb) target create "/Users/piaoyun/Desktop/xx.app/Contents/MacOS/xxxx"

遠端調試方法:

裝置端運作:

附加程序:

./debugserver *:1234 -a "YourAPPName"

直接啟動程序:

debugserver -x backboard *:1234 /path/to/app/executable

例:

debugserver -x backboard *:1234 /Applications/MobileNotes.app/MobileNotes

此指令會啟動記事本,并斷在dyld的第一條指令上

在Mac終端運作lldb指令後,輸入以下2條指令:

platform select remote-ios

process connect connect://你的裝置IP位址:1234

用USB連接配接方法:

wget http://cgit.sukimashita.com/usbmuxd.git/snapshot/usbmuxd-1.0.8.tar.bz2

tar xjfv usbmuxd-1.0.8.tar.bz2

cd usbmuxd-1.0.8/python-client/

python tcprelay.py -t 1234:1234

在Mac終端運作lldb指令後,輸入以下2條指令:

platform select remote-ios

process connect connect://localhost:1234

 -(void)loginWithUserName:(NSString *)username password:(NSString *)password

{

    NSLog(@"login.... username:%@   password:%@", username, password);  // 假設我們在此下斷點

}

一、基本操作

1.1.視圖層次

列印視圖層次 

po [self.contentView recursiveDescription]

1.2.改變某個取值

int a = 1;
//Console expr a=2
NSLog(@"實際值: %d", a);
           

1.3.call 改變view的背景色

call [self.view setBackgroundColor:[UIColor redColor]]
           

1.4.聲明變量

expr int $b=2 或者 e int $b=2
//輸出  po $b
           
print (type)表達式

例子:

print (int)$r6



print username
      

1.5.列印堆棧

ios中列印堆棧方法是 

NSThread callStackSymbols

,這裡調試的時候有個簡單的方法如下

bt  或者  bt all
           

1.6.更改方法傳回值

thread return NO/YES

-(BOOL) returnYES
{
    //thread return NO ,可以更改函數傳回值
    return YES;
}//方法傳回結果為NO  
           

1.7.多線程異常後檢視曆史對象的malloc配置設定曆史

先打開Enable Zombie Objects 和 Malloc Stack

(lldb) command script import lldb.macosx.heap
(lldb) malloc_info -S 0x7ff7206c3a70 
           

1.8.寄存器查找對象

曾經遇到過一個問題,

[self.tableview reloadData]

直接奔潰。這時候tableview其實沒有問題,我們要怎麼去找問題呢?

Xcode進階調試技巧(1)

如上圖所示,最後我們通過

malloc_info -S 0x00007fe99a629560

來檢視對象配置設定在堆裡面的具體的位址,随後在左側打開table的所有變量,輸入這個位址即能夠看到這個是什麼成員。

1.9.  c

繼續執行

7.s 

源碼級别單步執行,遇到子函數則進入

8.si

單步執行,遇到子函數則進入

9.n 

源碼級别單步執行,遇到子函數不進入,直接步過

10.ni

單步執行,遇到子函數不進入,直接步過

11.finish/f

退出子函數

12.thread list

列印線程清單

13.image lookup -a 表達式、image list

例子:

image lookup -a $pc

傳回如下:

      Address: debug[0x0000b236] (debug.__TEXT.__text + 1254)

      Summary: debug`main + 58 at main.m:16

列印加載子產品清單

image list [-f -o 通常帶這兩個參數使用]

傳回如下:

[  0] 40E417A4-F966-3DB4-B028-B0272DC016A7 0x000a0000 /Users/piao/Library/Developer/Xcode/DerivedData/debug-bdkhskdqykkoqmhjedilckzvpuls/Build/Products/Debug-iphoneos/debug.app/debug 

      /Users/piao/Library/Developer/Xcode/DerivedData/debug-bdkhskdqykkoqmhjedilckzvpuls/Build/Products/Debug-iphoneos/debug.app.dSYM/Contents/Resources/DWARF/debug

[  1] 011601C0-F561-3306-846B-94A7C8C841DA 0x2d9e6000 /Users/piao/Library/Developer/Xcode/iOS DeviceSupport/7.1.2 (11D257)/Symbols/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics

查找某個函數:

對于有調試符号的這樣使用

image lookup -r -n <FUNC_REGEX>

對于無調試符号的這樣使用:

image lookup -r -s <FUNC_REGEX>

14.disassemble -a 位址

例子:

dis -a $pc

debug`main at main.m:14:

   0xa71fc:  push   {r7, lr}

   0xa71fe:  mov    r7, sp

   0xa7200:  sub    sp, #0x24

   0xa7202:  movs   r2, #0x0

   0xa7204:  movt   r2, #0x0

   0xa7208:  str    r2, [sp, #0x20]

   0xa720a:  str    r0, [sp, #0x1c]

   0xa720c:  str    r1, [sp, #0x18]

   0xa720e:  blx    0xa7fe0                   ; symbol stub for: 

.

.

.

2015-04-29 添加

disassemble -A thumb    

可選:

thumbv4t

thumbv5

thumbv5e

thumbv6

thumbv6m

thumbv7

thumbv7f

thumbv7s

thumbv7k

thumbv7m

thumbv7em

///

15.memory read [起始位址 結束位址]/寄存器 -outfile 輸出路徑

例子:

memory read $pc

0x00035ebe: 0e 98 07 99 09 68 08 9a 90 47 0c 99 03 90 08 46  .....h...G.....F

0x00035ece: 03 99 01 f0 80 e8 02 22 c0 f2 00 02 41 f2 52 10  ......."....A.R.

memory read 0x35f1c 0x35f46 -outfile /tmp/test.txt  // 将記憶體區域儲存到檔案

2015-04-29添加:

預設情況下,memory read 隻能讀取 1024位元組資料

例如:

memory read 0x1000 0x3000 -outfile /tmp/test.txt 就會報錯

error: Normally, 'memory read' will not read over 1024 bytes of data.

解決方法:加-force參數

memory read 0x1000 0x3000 -outfile /tmp/test.txt -force

或者:

memory read 0x1000 -outfile /tmp/test.txt -count 0x2000 -force

memory read $x0(寄存器) -outfile /tmp/test.txt -count 0x2000 -force

--binary // 二進制輸出

例:

memory read 0x1000 0x3000 -outfile /tmp/test.bin --binary -force

寫記憶體:

memory write $rip 0xc3

memory write $rip+1 0x90

16.register read/格式、register write 寄存器名稱 數值

例子:

register read/x

傳回如下:

General Purpose Registers:

        r0 = 0x1599e028

        r1 = 0x38131621  libobjc.A.dylib`objc_msgSend + 1

        r2 = 0x000a85cc  "class"

        r3 = 0x000a85d4  (void *)0x000a8618: AppDelegate

        r4 = 0x00000000

        r5 = 0x000a71fd  debug`main + 1 at main.m:14

        r6 = 0x00000000

        r7 = 0x27d63c80

        r8 = 0x27d63c98

        r9 = 0x00000002

       r10 = 0x00000000

       r11 = 0x00000000

       r12 = 0x3a3ff1c8  (void *)0x3875cc19: _Unwind_SjLj_Unregister + 1

        sp = 0x27d63c5c

        lr = 0x38136eaf  libobjc.A.dylib`objc_autoreleasePoolPush + 311

        pc = 0x000a7236  debug`main + 58 at main.m:16

      cpsr = 0x20000030

// 改寫r9寄存器例子:

register write r9 2

17.display 表達式     undisplay 序号

例子:

display $R0

undisplay 1

18 記憶體斷點 watchpoint set expression 位址    /  watchpoint set variable 變量名稱 -- (源碼調試用到,略過)

例子:

watchpoint set expression 0x1457fa70

命中後得到結果:

Watchpoint 3 hit:

old value: 3

new value: 4

18.2 記憶體通路斷點 watchpoint set expression -w read -- 記憶體位址

watchpoint set expression -w read -- 0x16b9dd91

18.2 記憶體寫入斷點 watchpoint set expression -w write -- 記憶體位址

watchpoint set expression -w read -- 0x16b9dd91

18.3 條件斷點 watchpoint modify -c 表達式

例子:

watchpoint modify -c '*(int *)0x1457fa70 == 20'

命中後得到結果:

Watchpoint 3 hit:

old value: 15

new value: 20

19.找按鈕事件 po [按鈕名稱/記憶體位址 allTargets] 

例子:

(lldb) po [[self btnTest] allTargets]

{(

    <ViewController: 0x166af1f0>

)}

(lldb) po [[self btnTest] actionsForTarget:(id)0x166af1f0 forControlEvent:0]

<__NSArrayM 0x165b8950>(

testAction:

)

Bash

// 在機器上實戰一下:
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow:
 0x15e771c0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 
0x15e96210>; layer = <UIWindowLayer: 0x15e988e0>>
   | <UIView: 0x15eb4180; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x15eb4300>>
  
 |    | <UIButton: 0x15eb32d0; frame = (277 285; 46 30); opaque = NO;
 autoresize = RM+BM; layer = <CALayer: 0x15eb2e30>>
   |    
|    | <UIButtonLabel: 0x15db5220; frame = (0 6; 46 18); text = 
'Button'; opaque = NO; userInteractionEnabled = NO; layer = 
<_UILabelLayer: 0x15db5410>>
   |    | <_UILayoutGuide: 0x15eb4360; frame = (0 0; 0 20); hidden = YES; layer = <CALayer: 0x15eb4540>>
   |    | <_UILayoutGuide: 0x15eb4af0; frame = (0 568; 0 0); hidden = YES; layer = <CALayer: 0x15eb4b70>>

(lldb) po [(UIButton *)0x15eb32d0 allTargets]
{(
    <ViewController: 0x15e93250>
)}

(lldb) po [(UIButton *)0x15eb32d0 allTargets]
{(
    <ViewController: 0x15e93250>
)}

(lldb) po [(UIButton *)0x15eb32d0 actionsForTarget:(id)0x15e93250 forControlEvent:0]
<__NSArrayM 0x15dbfc50>(
testAction:
)
// 調用--
(lldb) po [0x15e93250 testAction:nil]
0x00210c18
           

// 再來一發,對按鈕屬性操作

Bash

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x15e771c0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x15e96210>; layer = <UIWindowLayer: 0x15e988e0>>
   | <UIView: 0x15eb4180; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x15eb4300>>
   |    | <UIButton: 0x15eb32d0; frame = (277 285; 46 30); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x15eb2e30>>
   |    |    | <UIButtonLabel: 0x15db5220; frame = (0 6; 46 18); text = 'Button'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15db5410>>
   |    | <_UILayoutGuide: 0x15eb4360; frame = (0 0; 0 20); hidden = YES; layer = <CALayer: 0x15eb4540>>
   |    | <_UILayoutGuide: 0x15eb4af0; frame = (0 568; 0 0); hidden = YES; layer = <CALayer: 0x15eb4b70>>

(lldb) expression UIButton *$btn = (UIButton *)0x15eb32d0
(lldb) expression [$btn setHidden:YES]
           

帶參數運作:

Bash

(lldb) target create "/bin/ls"
Current executable set to '/bin/ls' (x86_64).
(lldb) set set target.run-args  $(python \-c 'print "a"*200')
(lldb) run
Process 40752 launched: '/bin/ls' (x86_64)
ls: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: No such file or directory
Process 40752 exited with status = 1 (0x00000001)
(lldb)
           

std::string 讀取方式:從記憶體+8的地方開始  64bit自行變通

例:

Bash

(lldb) x $r0+8      
0x02db9248: 20 82 e3 14 71 00 00 00 61 00 00 00 c0 82 d3 14   .惝q...a...喇赢
0x02db9258: 71 00 00 00 2b 00 00 00 20 f9 e6 14 61 00 00 00  q...+... .a...
(lldb) x/s 0x14e38220
0x14e38220: "hello!piaoyun"
           

常見問題-列印無效

上面我們簡單的學習了如何使用LLDB指令。但有時我們在使用這些LLDB指令的時候,依然可能會遇到一些問題。不明類型或者類型不比對

p (void)NSLog(@"%@",[self.view  viewWithTag:1001])    //記住要加void 
p (CGRect)[self.view frame]  //記住不能寫成 self.view.frame,lldb的bug
           

二、調試進階

2.1.監聽某個方法的調用

Xcode進階調試技巧(1)

如果是自定義的view,比如

QQView

,想監聽frame變化,直接

[QQView setFrame:]

即可 系統方法就要如圖所示,x86_64系統中,rdi表示第一個參數,具體其他平台可看inspecting-obj-c-parameters-in-gdb,裡面有詳細的說明

$rdi ➡ arg0 (self)
$rsi ➡ arg1 (_cmd)
$rdx ➡ arg2
$rcx ➡ arg3
$r8 ➡ arg4
$r9 ➡ arg5
           

2.2.image尋址,找到崩潰行

此時會調用如下代碼會崩潰

NSArray *arr=[[NSArray alloc] initWithObjects:@"1",@"2", nil];
NSLog(@"%@",arr[2]);

2015-06-30 14:47:59.280 QQLLDB[6656:138560] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:
(
0   CoreFoundation                      0x000000010b2d8c65 __exceptionPreprocess + 165
1   libobjc.A.dylib                     0x000000010af71bb7 objc_exception_throw + 45
2   CoreFoundation                      0x000000010b1cf17e -[__NSArrayI objectAtIndex:] + 190
3   QQLLDB                              0x000000010aa404f6 -[ViewController viewDidLoad] + 1030
4   UIKit                               0x000000010ba75210 -[UIViewController loadViewIfRequired] + 738
5   UIKit                               0x000000010ba7540e -[UIViewController view] + 27
6   UIKit                               0x000000010b9902c9 -[UIWindow 
           

此時我們要直接找到崩潰的行數,很簡單。先找到非系統bug的崩潰位址,如下圖所示,顯然是第三行QQLLDB,右邊對應的位址是

0x000000010aa404f6

,然後輸入

image lookup --address 0x000000010aa404f6

,即可看到是60行出了bug

image lookup --address 0x000000010aa404f6  
Address: QQLLDB[0x00000001000014f6] (QQLLDB.__TEXT.__text + 1030)  
Summary: QQLLDB`-[ViewController viewDidLoad] + 1030 at ViewController.m:60
           

2.3.crash日志分析,讀取符号表

1.要知道如何讀取符号表,我們得先僞造一份符号表的資料檔案出來,代碼如下

NSArray *arr=[[NSArray alloc] initWithObjects:@"1",@"2", nil];
NSLog(@"%@",arr[2]);
           

僞造步驟:

1.編譯到真機

2.然後進入xcode将這個打開,找到QQLLDB.app.dSYM這個檔案,偷偷拷貝一份到桌面

3.然後在product中clean一下工程

4.在真機上打開一個編譯進去的程式,會出現圖

步驟如下圖所示:
Xcode進階調試技巧(1)
Xcode進階調試技巧(1)
Xcode進階調試技巧(1)
Xcode進階調試技巧(1)
Xcode進階調試技巧(1)

之後在指令行操作

atos -o /Users/tomxiang/Desktop/符号表/QQLLDB.app.dSYM/Contents/Resources/DWARF/QQLLDB -l0x1000e8000 0x1000eebd4
    //此時得到結果,告訴我們是ViewController的viewDidLoad的第62行崩潰
-[ViewController viewDidLoad] (in QQLLDB) (ViewController.m:62)
           

2.4觀察執行個體變量的變化

假設你有一個 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 中。

三、Chisel-facebook開源插件

1.安裝方法:

git clone https://github.com/facebook/chisel.git ~/.chisel
echo "command script import ~/.chisel/fblldb.py">>~/.lldbinit
           

安裝好後,打開xcode就可以運作調試了

2.基本指令

2.1.預覽圖檔
UIImage *image = [UIImage imageNamed:@"clear"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[viewB addSubview:imageView];

//此時執行指令之後,圖檔會被蘋果用自帶的預覽工具顯示出來    
visualize image
           
2.2.邊框/内容着色

如下所示,列印得到imageView位址之後,然後用

border

指令将其邊框着色

unborder

取消着色

(lldb) p imageView
(UIImageView *) $2 = 0x00007fb8c9b15910
(lldb) border 0x00007fb8c9b15910 -c green -w 2
           

同理,

mask

是給内容着色 

unmask

mask imageView -c green
           
2.3.關系鍊的繼承
(lldb) pclass image
UIImage
    | NSObject
           
2.4.列印所有屬性
`pinternals`這個指令就是列印出來的一個控件(id)類型的内部結構,詳細到令人發指!甚至是你自定義的控件中的類型,譬如這個styleView就是我自定義的,内部有個iconView的屬性,其中的值它也會列印出來。   

(lldb) pinternals image
(UIImage) $8 = {  
    NSObject = {
    isa = UIImage  
}
_imageRef = 0x00007fc1f3330780
_scale = 2
_imageFlags = {
 named = 1
 imageOrientation = 0
 cached = 0
 hasPattern = 0
 isCIImage = 0
 renderingMode = 0
 suppressesAccessibilityHairlineThickening = 0
 hasDecompressionInfo = 0
 }
}
           
2.5.bmessage

如果Chisel

ViewController沒有設定

viewWillDisappear

這個方法,此時我想用斷點斷下來,可以這樣

(lldb) bmessage -[ChiselViewController viewWillDisappear:]

Setting a breakpoint at -[UIViewController viewWillDisappear:] with condition (void*)object_getClass((id)$rdi) == 0x0000000100c47060
Breakpoint 2: where = UIKit`-[UIViewController viewWillDisappear:], address = 0x0000000101a0b566
           

Breakpoints

BreakPoint分類

breakpoint也是有分類的,我這裡的文章内大緻按使用的方式分為了

  • Normal Breakpoint,
  • Exception Breakpoint,
  • OpenGL ES Error breakpoint,
  • Symbolic Breakpoint,
  • Test Failure Breakpoint,
  • WatchPoints。

可以按具體的情景使用不同類型的breakpoint,解決問題為根本。

Normal Breakpoint

添加普通斷點就不多說了,在源代碼的右側點選一下即可。或者,使用快捷鍵:command + \ 來添加和删除。這兩種方式添加的breakpoints在Xcode上面是可以通過UI看到的。

還有可以通過下面兩個LLDB指令直接在運作時添加斷點,但是這種方式需要注意的是一方面無法通過UI直接看到斷點,另外一方面隻存在于本次運作,下一次啟動模拟器重新運作的時候,這些斷點就不生效了。

Xcode進階調試技巧(1)

如上圖,通過“br li”指令列印所有的breakpoint,可以看到一共有3個breakpoint,第一個是通過Xcode的UI添加的,後面兩個分别是通過下面兩個指令添加的:

“breakpoint set -f XXX.m -l XX” 和  “b XXX.m:XX”。

breakpoint set -n write -c "(*(char**) ($esp + 8))[0]==0x17 
    && (*(char**) ($esp + 8))[1]==0x03 
    && (*(char**) ($esp + 8))[2]==0x03 
    && (*(char**) ($esp + 8))[3]==0x00
    && (*(char**) ($esp + 8))[4]==0x28"      

Exception Breakpoint

可以通過下圖中Xcode的UI添加Exception Breakpoint。有時候,比如數組越界或者設定一個空對象等問題,都會抛出一個異常,但是這種類型的錯誤非常難以定位,這個時候就可以使用Exception Breakpoint來進行調試,在異常發生時可以捕捉到并停止程式的執行。OC中的異常是一個常被忽略的地方,但實際上系統架構内這個使用非常廣泛,大部分這種錯誤資訊,系統架構都會以異常的形式throw出來,是以善用這種breakpoint的話,我們能大大減少查找錯誤的時間。

Xcode進階調試技巧(1)
例如,當我們添加如下Exception Breakpoint之後(bt 指令後文中會講解,這個指令的作用是在斷點觸發時,列印回調棧資訊):
Xcode進階調試技巧(1)
類似下面這樣的數組越界的問題,我們可以很容易就定位到問題所在,不用再毫無頭緒找來找去了:
Xcode進階調試技巧(1)
當斷點暫停執行時,我們可以通過Xcode的UI中檢視調用棧資訊:
Xcode進階調試技巧(1)
或者檢視bt指令列印的調用棧資訊:
Xcode進階調試技巧(1)
還有類似如下的錯誤可以通過這種斷點很容易定位到:
Xcode進階調試技巧(1)

,不過這種問題,可以通過使用setValue:forKey:代替來避免。

OpenGL ES Error Breakpoint

同上圖中,在Xcode的breakpoint navigator的下部添加按鈕,選擇”Add OpenGL ES Error Breakpoint”即可。這個breakpoint主要是用來在OpenGL ES發生錯誤時停止程式的運作。

Symbolic Breakpoint

通過Xcode的UI添加symbolic breakpoint的方式同exception breakpoint,彈出框如下:

Xcode進階調試技巧(1)

Symbolic breakpoints 在某個特定的函數或者方法開始執行的時候,暫停程式的執行,通過這種方式添加斷點,我們就不需要知道在源檔案中添加,也不需要知道斷點設定在檔案的第幾行。

上圖中,最主要的設定是Symbol的内容,可以有如下幾種:

  • 1. A method name,方法名稱,例如 pathsMatchingExtensions: 這樣的方法名稱,會對所有類的這個方法都起作用。
  • 2. A method of a particular class. 特定類的某個方法。例如 ,[SKLine drawHandlesInView],或者 people::Person::name()
  • 3. A function name。函數名稱。例如 ,_objc_msgForward 這樣C函數。
另外,也可以通過指令行的方式添加 Symbolic breakpoints。對C函數添加斷點:
Xcode進階調試技巧(1)
對OC的方法添加斷點:
Xcode進階調試技巧(1)

常用的這個類型的斷點有,objc_exception_throw可以用來代替 Exception Breakpoint,還有一個-[NSObject doesNotRecognizeSelector:] 也比較常用,用于檢測方法調用失敗。

下斷:

breakpoint set -a 函數位址   --正常斷點

breakpoint set --func-regex 函數關鍵字   --飄雲提示:這個非常有用!我也是最近才研究發現的-雖然官方文檔一直有,但是沒重視

這樣下斷的優勢:

比如再某動态庫中有 testA函數,那麼正常做法是先 image list -o -f 檢視子產品基址 然後 image lookup -r -n 函數關鍵字找到偏移   然後再 br s -a 基址+偏移!

用上面這個指令下端就簡潔友善了!!!lldb會自動幫你下斷所有比對特征字的斷點,可以模糊比對哦

再來一個對動态庫函數下斷的:

breakpoint set --shlib foo.dylib --name foo

這個也非常有用,可以進行斷點過程中的一些自動化處理:

breakpoint command add 斷點序号

這個也非常有用,對C函數下斷非常好 / 貌似是模糊比對

breakpoint set -F isTest   / 可以簡寫為 b isTest

Test Failure Breakpoint

通過Xcode的UI添加方法同上。這個類型的break point 會在 test assertion 失敗的時候暫停程式的執行。

Watchpoints

Watuchpoints是一個用來監聽變量的值的變化或者記憶體位址的變化的工具,發生變化時會在debugger中觸發一個暫停。對于那些不知道如何準确跟蹤的狀态問題,可以利用這個工具來解決。要設定watchpoint的話,在程式運作到stack frame包含有你想觀察的變量時,讓debugger暫停運作,這個時候變量在目前stack frame的scope内,這個時候才能對該變量設定watchpoint。

你可以在Xcode的GUI中設定watchpoint,在xcode的 Variables View中,把你想觀察的變量保留出來,然後右鍵設定“Watch XXX”。例如下圖,觀察self的title變量,點選 Watch “_button1ClickCount” 即可。

Xcode進階調試技巧(1)

指令行

或者也可以通過指令行來設定watchpoint:watch set variable _button1ClickCount,詳細指令可以參考:http://lldb.llvm.org/lldb-gdb.html,有好幾種指令可以達到同樣的效果。

上面是對變量進行觀察,實際上我們可以對任意記憶體位址進行觀察,指令如下:watchpoint set expression — 0x123456,參考:http://stackoverflow.com/questions/21063995/watch-points-on-memory-address

需要注意的是,watchpoint是分類型的,包括read,write或者read_write類型,這個非常容易了解,在讀,寫或者讀寫變量或記憶體的時候,watchpoint是否被觸發。read,write或read_write跟着-w參數後面表示類型。另外,指令行中,watchpoint還有一些簡寫,set簡寫為s,watch簡寫為wa,variable簡寫為v。

下面的示例是來自 http://www.dreamingwish.com/article/lldb-usage-a.html 網站的幾個指令:

Xcode進階調試技巧(1)

第一個指令是監聽_abc4變量的記憶體位址write的變化,第二個是監聽_abc4變量read的變化,第三個是監聽_abc3變量read_write的變化。

需要注意的是,通過Xcode的GUI添加的watchpoint為預設類型,即write類型,如果想要添加讀寫都watch的watchpoint,則隻能通過指令行工具進行添加了。

使用watchpoint modify -c ‘(XXX==XX)’,則修改watchpoint之後在某個值的時候才會監聽。

編輯選項

BreakPoint Condition

當我們通過Xcode對breakpoint進行編輯時,可以發現normal breakpoint和symbolic breakpoint都有一個”Condition”輸入選項,這個的作用很容易了解,隻有在設定的condition表達式為YES的情況下這些斷點才會起作用。

例如,下圖中的breakpoint在判斷字元串相等的時候才會停止運作:

Xcode進階調試技巧(1)

可以注意到這裡使用stringWithUTF8Stirng:方法,原因在于lldb的expression parser有一個bug,不相容非ASCII字元,需要處理一下才行,否則會報錯“An Objective-C constant string's string initializer is not an array”,參考:http://stackoverflow.com/questions/17192505/error-in-breakpoint-condition

更加簡單一些的例子就不說了,比如 i == 99之類的簡單比較,隻要表達式的結果為BOOL類型即可。

Breakpoint Actions

可以看到上面的每種breakpoint編輯選項中基本上都有“Add Action”選項,當breakpoint被觸發時,都首先會執行我們設定的這些action,然後我們才能得到控制權,即Xcode上面才會顯示程式停止執行的UI。這個Action通過例子比較好了解,我們通過上面那個setObject:forKey:的異常來說明。代碼如下:

Xcode進階調試技巧(1)
設定Breakpoint:
Xcode進階調試技巧(1)

可以看到上圖中,我們一共設定了3個action。第一個action,用來列印exception的詳細資訊,用法參考:http://stackoverflow.com/questions/17238673/xcode-exception-breakpoint-doesnt-print-details-of-the-exception-being-thrown。

第二個action,我們使用shell指令“say”,讓電腦發聲,把一段文字讀出來。

第三個action,我們使用“bt”指令來列印調用棧資訊

設定完成之後,當異常發生時,我們會聽到電腦發聲念上圖中的英文,然後在log中可以看到如下資訊,第一行是Exception的描述資訊,下面是調用堆棧:

Xcode進階調試技巧(1)

Continuing after Evaluation

看一下breakpoint的編輯彈窗,我們可以發現有一個 “Automatically continue after evaluation actions” checkbox選項。當我們勾選這個checkbox之後,debugger會執行breakpoint中添加的所有的actions,然後繼續執行程式。對于我們來說,除了觸發一大堆command并且執行時間很長的情況之外,程式會很快跳過這個breakpoint,是以我們可能根本不會注意到這個breakpoint的存在。是以,這個選項的功能相當于在執行的最後一個action之後,直接輸入continue指令繼續執行。

有了這個很強大的功能,我們可以直接通過breakpoints來單獨對我們的程式進行修改。在某行代碼時停止執行,使用”expression”指令來直接修改程式的某個變量設定直接修改UI,然後繼續執行。expression / call 配合這個選項的時候,會非常強大,可以很友善實作很多很強大的功能。

例如,我們實作一個如下的功能,把tableview的第一個cell的selectBackgroundView的背景色改為紅色:

Xcode進階調試技巧(1)
action的内容為“expression [[cell selectedBackgroundView] setBackgroundColor:[UIColor redColor]]”,這裡的表達式先不用關心,我們後面LLDB章節會講到,修改之後,當我們點選cell的時候,cell的背景就會如下圖一樣變紅:
Xcode進階調試技巧(1)
使用這種方式,我們在不需要修改一行代碼的情況下,隻需要通過修改breakpoint,就可以實作對UI的各種調試效果。

前言

你是否嘔心瀝血的嘗試去了解代碼和列印出來的變量内容?
NSLog(@"%@", whatIsInsideThisThing);
           
或是漏過函數調用來就簡化工程行為?
NSNumber *n = @7; // theFunctionThatShouldReallyBeCalled();
           
或者短路的檢查邏輯?
if ( || theBooleanAtStake) { ... }
           
亦或者是函數的僞實作?
int calculateTheTrickyValue {
  return ;

  /*
   Figure this out later.
   ...
}
           

那是不是要不斷的重編譯,然後又開始新的輪回?

建構軟體是複雜的而且BUG無處不藏。一個正常的修正過程是修改代碼,編譯,再次運作,然後祈禱上帝。

似乎也不用墨守成規。你可以用調試器啊!假設你已經知道怎麼檢視變量值,這裡有更多你需要掌握的東西。

這篇文章的目的是挑戰你的調試知識,把你可能知道得基礎知識點解析的更透徹,然後向你展示了一系列有趣的栗子。開始吧!

LLDB

LLDB是個開源調試器,REPL特性,自帶C++以及Python插件。它與Xcode綁定并且駐在控制台界面化于視窗的下端。

調試器允許你在一個特定執行時刻暫停程式,檢視變量值,執行自定義指令,以及按你認為合适得步驟進行程式步驟操控。(調試器主要功能戳這裡)

你以前使用調試器的部分很可能僅限于Xcode的UI上打個斷點。但是這有些技巧,你可以做一些更酷比的事情。通過GDB與LLDB之間對比是針對所有支援的指令行的一個很好鳥瞰式的學習法,你還可能想要去安裝Chisel,一套開源的LLDB插件讓你的調試更加有趣。

與此同時,讓我們開始如何使用調試器列印變量值的旅程吧。

基礎

這裡有一個簡單短小的程式來列印字元串。注意到斷點被添加到了第八行:
Xcode進階調試技巧(1)
程式到此會停下來然後打開控制台,讓我們能與調試器進行互動。此時我們應該輸入什麼呢?

幫助

最簡單得指令是鍵入

help

,你可以擷取一個指令行清單。如果你忘記一個指令或者想知道該指令更細緻的使用方法,那麼你可以通過調用

help <command>

,比如

help print

help thread

。如果你甚至忘記了指令本身,你可以嘗試使用

help help

,但是如果你懂得足夠多,你可能已經徹底不要這個指令了。

列印

列印值很容易,隻要試着鍵入

print

指令:
Xcode進階調試技巧(1)
LLDB實際上支援字首指令判斷,是以你同樣可以使用

prin

pri

或者

p

。但是你不能使用

pr

,因為LLDB不能分辨出你是否是想執行

process

指令。(吐槽幸好p沒有歧義,暴露屬性)

你同時也注意到了結果帶一個$0。實際上你可以用這個來引用變量!試着鍵入

$0 + 7

然後你會看到106。任何帶美元符号是LLDB的命名空間,其存在是為了為你提供幫助。

表達式

如果你想修改一個值?修改,你說的算?好吧,修改!下面來一個簡單得表達式指令行:
Xcode進階調試技巧(1)

這并不修改調試器中的值。實際上修改的是程式中的值!如果你繼續程式,它很神奇地會列印出42紅氣球(上下文)。

從現在開始注意一點,我們為了友善用

p

e

代替

print

expression

什麼是列印指令?

這裡有一個有意思的表達式來考慮下:

p count = 18

。如果我們執行指令然後列印count的内容,我們會看到它确實相當于執行了表達式

count = 18

這兩者的差別是

print

指令不帶參數,這點與

expression

不同。考慮

e -h +17

。在選擇是否要進行輸入源為

+17

,帶

-h

标志的操作,還是選擇是否要進行計算區分

17

h

操作,在這兩個選擇上面是不明确的。調試器認為連字元導緻了混淆,你可能得不到想要的結果。

幸運的是,這個解決方法十分簡單。使用

--

來表示表示符号的結束以及輸入源的開始。此時如果你想要用

-h

标志,你可以使用

e -h -- +17

,如果你想要進行區分,則你可以執行

e -- -h +17

。不帶标志則是十分普通,它(

e --

)有一個别名

print

如果你鍵入

help print

并且往下拖拽,你會看到:

列印對象

如果我們嘗試鍵入
p objects
           

那輸出會有點冗繁:

當嘗試列印一個更加複雜的資料結構時候會情況會更糟:

(lldb) p @[ @"foo", @"bar" ]

(NSArray *) $8 =  @"2 objects"
           
好吧,我們想看下對象的description方法。我們需要告訴

expression

指令作為對象來列印這個結果,使用-O标志(這不是0):
(lldb) e -O -- $
<__NSArrayI >(
foo,
bar
)
           
很走運,

e -O --

也有别名,其别名為

po

,我們可以隻要這樣使用:
(lldb) po $
<__NSArrayI >(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $ =  @"lunar"
           

列印變量

print

指令有許多種不同的格式可以由你來指定。它們以指令格式為

print/<fmt>

或者更簡單

p/<fmt>

。接下來舉個栗子。

預設的格式:

(lldb) p 

           
16進制格式:
(lldb) p/x 
x10
           
二進制格式(t代表tow):
(lldb) p/t 

(lldb) p/t (char)

           
你還可以使用

p/c

列印字元,或者是

p/s

列印一個非終止類型的字元串

char *

。完整清單戳這裡。

變量

至此你可以列印對象跟簡單得類型,并可以在調試器中使用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:]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:]

           
變量特性讓調試器更容易被使用,你這麼認為嗎?

流程控制

你的程式會在你打上斷點的位置停下來。

此時你看到在調試工具欄有四個按鈕,通過使用它們你可以控制程式的執行流程:

Xcode進階調試技巧(1)

這四個按鈕從左到右依次為:繼續,單步,跳入,跳出。

首先,繼續按鈕将會讓你得程式繼續正常執行(可能一直運作或者遇到下一個斷點)。在LLDB中,你可以使用

process continue

來繼續執行,别名為

c

其次,單步執行将會将單行代碼當做黑盒一樣執行。如果那行你調用了函數,那将不會進入這個函數,而是直接執行這個函數後繼續運作。LLDB中相對應的指令是

thread step-over

next

,或者 

n

如果你想進入一個函數調用來檢查調試該函數的執行,你可以使用第三個按鈕,跳入,LLDB同樣提供了

thread step-in

step

, 和

s

。注意到

next

step

在目前行代碼不涉及函數調用的時候效果是一樣的。

大部分知道

c,n,s

。但是還有第四個按鈕,跳出。如果你不小心跳入了一個函數而你本意是想跳過它,一般反應是不斷的按n知道函數傳回。跳出幫你節省時間。它會執行到

return

語句(知道執行了出棧操作),然後會停下來。

舉個栗子

來看下如下的代碼片段:
Xcode進階調試技巧(1)
代碼停在斷點,然後我們執行如下的指令行:
p i
n
s
p i
finish
p i
frame info
           
這裡,

frame info

将會告訴你目前行以及源檔案是啥,可以通過鍵入

help frame

help thread

,以及

help process

擷取更多資訊。那麼輸出什麼呢?先思考之前的描述想下答案!
(lldb) p i
(int) $ = 
(lldb) n
-- :: DebuggerDance[:]  is odd!
(lldb) s
(lldb) p i
(int) $ = 
(lldb) finish
-- :: DebuggerDance[:]  is even!
(lldb) p i
(int) $ = 
(lldb) frame info
frame #: x000000010a53bcd4 DebuggerDance`main +  at main.m:
           
仍在17行的原因是

finish

指令會讓程式運作直到

isEven()

函數傳回,然後馬上停止。但是請注意,17行已經執行完了。

線程傳回

還有一個特别幫的功能是你在調試的時候可以用

thread return

來控制程式流程。它使用可選參數,将這個參數載入寄存器,單後馬上執行傳回指令,然後函數出棧。這意味着剩下函數沒有被執行。這樣因為ARC的引用計數/記錄出現問題,或者遺漏一些清除操作。但在一個函數的開頭執行這個指令是一個非常棒得函數打樁并且反悔了一個僞結果。

讓我們來對上述相同的代碼段跑如下的指令:

p i
s
thread return NO
n
p even0
frame info
           
在看答案之前鄉下結果,答案如下:
(lldb) p i
(int) $ = 
(lldb) s
(lldb) thread return NO
(lldb) n
(lldb) p even0
(BOOL) $ = NO
(lldb) frame info
frame #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17
           

斷點

我們一直都使用斷點來讓程式停止,檢視目前狀态進而捕獲BUG。但是如果我們轉變對斷點的了解,我們可以獲得更多可能。
A breakpoint allows you to instruct a program when to stop, and then allows the running of commands.
考慮在函數剛開始處打一個斷點,使用

thread return

來重寫函數行為,然後繼續。現在想象下自動實作這種處理。是不是聽起來很牛X,不是麼?

斷點管理

Xcode提供了一套工具來建立和操作斷點。我們将會逐一過一遍并且進行描述與之對應的LLDB指令行。

在Xcode的左面闆上,有一堆按鈕集合。有一個長得很像斷點。點選打開斷點導航欄,進去之後你一眼看到你所操作的所有斷點:

Xcode進階調試技巧(1)
這裡你可以看到所有的斷點 - 對應LLDB中的

breakpoint list

或者是

br li

。你可以點選單個斷點進行打開或者關閉 - 對應LLDB中的

breakpoint enable <breakpointID>

breakpoint disable <breakpointID>

(lldb) br li
Current breakpoints:
: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = , locations = , resolved = , hit count = 

  .: where = DebuggerDance`main +  at main.m:, address = , resolved, hit count = 

(lldb) br dis 
 breakpoints disabled.
(lldb) br li
Current breakpoints:
: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = , locations =  Options: disabled

  .: where = DebuggerDance`main +  at main.m:, address = , unresolved, hit count = 

(lldb) br del 
 breakpoints deleted; 0 breakpoint locations disabled.
(lldb) br li
No breakpoints currently set.
           

建立斷點

(UI建立略了。。。是人都會吧。。)

在調試器中打斷點,使用

breakpoint set

指令:
(lldb) breakpoint set -f main.m -l 
Breakpoint : where = DebuggerDance`main +  at main.m:, address = x
           
縮寫可以用

br

b

是另外一個完全不同的指令,是

_regexp-break

的别名,但是它足夠健壯來進行建立上述指令一樣效果的斷點:
(lldb) b main.m:
Breakpoint : where = DebuggerDance`main +  at main.m:, address = x
           
你也可以防止一個斷點在一個符号(C語言函數),而不用指定行數:
(lldb) b isEven
Breakpoint : where = DebuggerDance`isEven +  at main.m:, address = 
(lldb) br s -F isEven
Breakpoint : where = DebuggerDance`isEven +  at main.m:, address

           
現在這些斷點會停止正在将要執行的函數,同樣适用與OC方法:
(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint : where = CoreFoundation`-[NSArray objectAtIndex:], address = 
(lldb) b -[NSArray objectAtIndex:]
Breakpoint : where = CoreFoundation`-[NSArray objectAtIndex:], address = 
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
Breakpoint : where = CoreFoundation`+[NSSet setWithObject:], address = 
(lldb) b +[NSSet setWithObject:]
Breakpoint : where = CoreFoundation`+[NSSet setWithObject:], address = 
           
如果你想通過UI來建立象征性斷點,你可以點選左下端斷點導航欄的+号:
Xcode進階調試技巧(1)
然後選擇第三個選項:
Xcode進階調試技巧(1)
此時出現彈出框讓你輸入比如

-[NSArray objectAtIndex:]

的符号,然後程式在這個函數調用的時候便可以停止下來,不管是你的代碼或者還是大蘋果的代碼!

如果我們看下其他選項,我們可以發現一些有意思的選項,同樣提供了各種條件觸發的鍛煉隻要你點選了Xcode的UI并且選擇了“Edit Breakpoint”選項:

Xcode進階調試技巧(1)

如上圖,斷點隻有在i為99的時候才會停止程式。你可以同樣設定“ ignore”選項來告訴斷點在前n次調用的時候不用停止程式(條件為真)。

這裡還有一個“Add Action”按鈕。。。

斷點動作

可能上面斷點的栗子中,你想知道每次斷點時候i值是多少。我們可以使用動作

p i

,然後當斷點觸發的時候我們進入調試器,它會預先執行這個指令在将控制流程交給你之前:
Xcode進階調試技巧(1)
你也可以加多重動作,可以是調試器指令,shell指令或者更健壯的列印資訊:
Xcode進階調試技巧(1)

如上你可以看到列印出i值,還有強調語句,列印出自定義的表達式。

下面是上述功能用純LLDB指令代替Xcode的UI:

(lldb) breakpoint set -F isEven
Breakpoint : where = DebuggerDance`isEven +  at main.m:, address = 
(lldb) breakpoint modify -c 'i == 99' 
(lldb) breakpoint command add 
Enter your debugger command(s).  Type 'DONE' to end.
> p i
> DONE
(lldb) br li 
: name = 'isEven', locations = , resolved = , hit count = 
    Breakpoint commands:
      p i

Condition: i == 

  .: where = DebuggerDance`isEven +  at main.m:, address = , resolved, hit count = 
           
自動化,我們來了!

計算值之後繼續

如果視線停留在斷點彈出框的底端,你會額外看到一個選項:“Automatically continue after evaluation actions(計算動作後自動執行)。”它隻是一個勾選框,但是它卻有強大的能力。如果你勾選上了,調試器将會蘋果你所有的指令然後繼續執行程式。表面上看上跟斷點沒有打住一樣(除非你斷點太多了,拖慢了程式進度)。

這個勾選框功能與最後一個動作斷點繼續執行效果一樣,但是有勾選框更加容易點。對應調試器的指令如下:

(lldb) breakpoint set -F isEven
Breakpoint : where = DebuggerDance`isEven +  at main.m:, address = 
(lldb) breakpoint command add 
Enter your debugger command(s).  Type 'DONE' to end.
> continue
> DONE
(lldb) br li 
: name = 'isEven', locations = , resolved = , hit count = 
    Breakpoint commands:
      continue

  .: where = DebuggerDance`isEven +  at main.m:, address = , resolved, hit count = 
           
計算後自動繼續運作讓你可以單獨通過使用斷點來修改你的程式!你可以停止在單行,運作一個

expression

指令來改變變量,然後繼續。

舉個栗子

考慮下簡陋殘酷的“列印式調試”技術。不是用:
NSLog(@"%@", whatIsInsideThisThing);
           

而是用斷點處設定列印變量值替代吊列印日志列印語句然後繼續。

不是用:

int calculateTheTrickyValue {
  return ;

  /*
   Figure this out later.
   ...
  */
}
                
而是用斷點處調用

thread return 9

然後繼續執行。

帶動作的象征斷點确實真的很強大。你也可以添加這些斷點到你朋友的Xcode工程并且讓動作将所有資訊細緻展示出來。接下來看看要耗時多久來進行計算以及會發生什麼吧。

調試器完整操作

在起舞之前還有一點需要我們注意。你真的可以在調試器中執行任何的C/OC/C++/Swift指令。比較弱的是我們不能建立一個新的函數。。。這意味着沒有新的類,塊,函數,帶虛方法的C++類等等。除了這個,調試器什麼都能滿足!

我們可以配置設定一些位元組:

(lldb) e char *$str = (char *)malloc()
(lldb) e (void)strcpy($str, "munkeys")
(lldb) e $str[] = 'o'
(char) $ = 'o'
(lldb) p $str
(char *) $str =  "monkeys"
           
或者我們可以檢查一些記憶體(使用

x

指令)來看我們新數組的4個位元組:
(lldb) x/c $str
: monk
           
我們還可以後三個位元組:
(lldb) x/w `$str + `
x7fd04a900043: keys
           
當你所要的活結束的時候别忘記了釋放記憶體避免造成記憶體洩露:
(lldb) e (void)free($str)
           

跳舞吧,騷年!

現在我們已經清楚基礎步驟,是時候來整一些比較瘋狂的東西了。我過去曾寫過一篇部落格(大家自己收藏。。。)發表在looking at the internals of NSArray。當時用了大量的NSLog語句,後來全用調試器搞定了。它是一個很好的調試器使用練習。

暢通無阻(無斷點模式)

當你的應用在跑的時候,Xcode中的調試工具欄展示一個停止按鈕而非繼續狀态的按鈕:
Xcode進階調試技巧(1)
選中這個按鈕的時候,應用遇到斷點将會停止(就像輸入了

process interrupt

)。這時候将會讓你進入調試器。

這裡有一個有趣的地方。如果你運作一個iOS應用,你可以嘗試這個(全局變量可提供)

(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

然後,通過上述的輸出,我們可以看到隐藏的視圖:
(lldb) e id $myView = (id)
           
然後在調試器中修改它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
           

在你下次繼續運作這個程式的時候你才會看到變化。這因為這個變化需要傳遞給渲染服務然後視圖展示才會被更新。

渲染服務實際上是另一個程序(稱作背景),并且甚至我們調試程序被停止了,這個背景也不會被停止!

這意味着不通過繼續,你可以執行:

(lldb) e (void)[CATransaction flush]
           
在模拟器中或者裝置中的UI會進行重新整理而你還在調試器中!Chisel提供了一個别名函數叫做

caflush

,并且它被用來實作其它捷徑像

hide <view>

show <view>

還有其他許多許多。所有的Chisel指令都有對應的文檔,是以就在安裝它之後鍵入

help

來随心所欲的擷取更多的資訊吧。

壓入視圖控制器

想象一個簡單的應用有一個UINavigationController作為根視圖控制器。你可以在調試器中相當簡易的執行如下操作:

然後壓入子視圖控制器:

(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]
           

最後執行:

你會看到馬上壓入了一個視圖控制器。

找到按鈕的目标

想象下你調試器中有一個變量,$myButton,你想要去建立它,并從UI中抓取它,或者簡單地隻是你想在斷點停下來的時候将它作為個局部變量。你可能想知道當你點選它的時候是誰接收了這個動作。這裡展示達到這點有多麼的簡單:
(lldb) po [$myButton allTargets]
{(
    <MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)x7fb58bd2e240 forControlEvent:]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)
           
現在你可能想在事件發生的時候添加一個斷點。隻要在LLDB或者Xcode設定象征性斷點在

-[MyEventListener _handleTap:]

。and you are all set to go!

觀察執行個體變量值變化

想象一個假設的場景你有一個

UIView

且它的

_layer

執行個體變量被重寫了。因為這裡可能不涉及方法,我們不能使用象征性斷點。取而代之的是我們想觀察一個記憶體位址什麼時候被寫入了。

首先我們需要找到

_layer

對象在那裡:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $ = 
                
現在我們知道($myView + 8)這個記憶體位址被寫入了:
(lldb) watchpoint set expression -- (int *)$myView + 
Watchpoint created: Watchpoint : addr = x7fa554231340 size =  state = enabled type = w
    new value: x0000000000000000
           
對應Chisel裡面的

wivar $myView _layer

在非重寫方法上的象征性斷點

想象你想知道什麼時候

-[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找到它)。這是個通過的曆程,因為你知道已經知道有四種體系架構了。吐槽略。。幸運的是,Chisel已經完成了這些封裝,你可以調用

bmessage

(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 
Breakpoint : where = UIKit`-[UIViewController viewDidAppear:], address = 
           

LLDB與Python

LLDB有完整的内置Python支援。如果你在LLDB上輸入腳本,它會打開一個Python REPL。如果你在LLDB中鍵入

script

,它會打開一個Python REPL。你可以傳入一行Python語句到

script

指令來不進入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來執行這些字元串。聽起來很簡單吧!呃?

參考連結:

1.當異常出現時

2.日志記錄CocoaLumberjack

3.在Xcode中調試程式

4.南峰子的技術部落格

5.與調試器共舞 - LLDB 的華爾茲

6.官方調試技巧文檔

7.inspecting-obj-c-parameters-in-gdb