Xcode高级调试技巧(1)
在蘋果的官方文檔中列出了我們?cè)谡{(diào)試中能用到的一些命令,我們?cè)谶@重點(diǎn)講一些常用的命令
調(diào)試本地文件方法(Mac OS X):(lldb) target create?"/Users/piaoyun/Desktop/xx.app/Contents/MacOS/xxxx"
遠(yuǎn)程調(diào)試方法:
設(shè)備端運(yùn)行:
附加進(jìn)程:
./debugserver *:1234 -a "YourAPPName"
直接啟動(dòng)進(jìn)程:
debugserver -x backboard *:1234 /path/to/app/executable
例:
debugserver -x backboard *:1234?/Applications/MobileNotes.app/MobileNotes
此命令會(huì)啟動(dòng)記事本,并斷在dyld的第一條指令上
在Mac終端運(yùn)行l(wèi)ldb命令后,輸入以下2條命令:
platform select remote-ios
process connect connect://你的設(shè)備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終端運(yùn)行l(wèi)ldb命令后,輸入以下2條命令:
platform select remote-ios
process connect connect://localhost:1234
?-(void)loginWithUserName:(NSString *)username password:(NSString *)password
{
? ? NSLog(@"login.... username:%@ ? password:%@", username, password); ?// 假設(shè)我們?cè)诖讼聰帱c(diǎn)
}
一、基本操作
1.1.視圖層次
打印視圖層次?po [self.contentView recursiveDescription]
1.2.改變某個(gè)取值
int a = 1; //Console expr a=2 NSLog(@"實(shí)際值: %d", a);1.3.call 改變view的背景色
call [self.view setBackgroundColor:[UIColor redColor]]1.4.聲明變量
expr int $b=2 或者 e int $b=2 //輸出 po $bprint (type)表達(dá)式
例子:
print (int)$r6
print username
1.5.打印堆棧
ios中打印堆棧方法是?NSThread callStackSymbols,這里調(diào)試的時(shí)候有個(gè)簡(jiǎn)單的方法如下
bt 或者 bt all1.6.更改方法返回值
thread return NO/YES
-(BOOL) returnYES {//thread return NO ,可以更改函數(shù)返回值return YES; }//方法返回結(jié)果為NO1.7.多線程異常后查看歷史對(duì)象的malloc分配歷史
先打開Enable Zombie Objects?和?Malloc Stack
(lldb) command script import lldb.macosx.heap (lldb) malloc_info -S 0x7ff7206c3a701.8.寄存器查找對(duì)象
曾經(jīng)遇到過(guò)一個(gè)問(wèn)題,[self.tableview reloadData]直接奔潰。這時(shí)候tableview其實(shí)沒(méi)有問(wèn)題,我們要怎么去找問(wèn)題呢?
如上圖所示,最后我們通過(guò)malloc_info -S 0x00007fe99a629560來(lái)查看對(duì)象分配在堆里面的具體的地址,隨后在左側(cè)打開table的所有變量,輸入這個(gè)地址即能夠看到這個(gè)是什么成員。
1.9. ?c
繼續(xù)執(zhí)行
7.s?
源碼級(jí)別單步執(zhí)行,遇到子函數(shù)則進(jìn)入
8.si
單步執(zhí)行,遇到子函數(shù)則進(jìn)入
9.n?
源碼級(jí)別單步執(zhí)行,遇到子函數(shù)不進(jìn)入,直接步過(guò)
10.ni
單步執(zhí)行,遇到子函數(shù)不進(jìn)入,直接步過(guò)
11.finish/f
退出子函數(shù)
12.thread list
?
打印線程列表
?
13.image lookup -a 表達(dá)式、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 通常帶這兩個(gè)參數(shù)使用]
返回如下:
[? 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
查找某個(gè)函數(shù):
對(duì)于有調(diào)試符號(hào)的這樣使用
image lookup -r -n <FUNC_REGEX>
對(duì)于無(wú)調(diào)試符號(hào)的這樣使用:
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 [起始地址 結(jié)束地址]/寄存器?-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 ?// 將內(nèi)存區(qū)域保存到文件
2015-04-29添加:
默認(rèn)情況下,memory read 只能讀取 1024字節(jié)數(shù)據(jù)
例如:
memory read 0x1000 0x3000 -outfile /tmp/test.txt?就會(huì)報(bào)錯(cuò)
error: Normally, 'memory read' will not read over 1024 bytes of data.
解決方法:加-force參數(shù)
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 // 二進(jìn)制輸出
例:
memory read 0x1000 0x3000 -outfile /tmp/test.bin?--binary?-force
寫內(nèi)存:
memory write?$rip 0xc3
memory?write?$rip+1 0x90
16.register read/格式、register?write 寄存器名稱 數(shù)值
例子:
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?表達(dá)式?????undisplay?序號(hào)
例子:
display $R0
undisplay?1
?
18 內(nèi)存斷點(diǎn)?watchpoint set expression 地址?? ?/ ?watchpoint set variable?變量名稱 -- (源碼調(diào)試用到,略過(guò))
例子:
watchpoint set expression?0x1457fa70
命中后得到結(jié)果:
Watchpoint 3 hit:
old value: 3
new value: 4
18.2 內(nèi)存訪問(wèn)斷點(diǎn)?watchpoint set?expression?-w read -- 內(nèi)存地址
watchpoint set expression -w read -- 0x16b9dd91
18.2 內(nèi)存寫入斷點(diǎn)?watchpoint set?expression?-w write -- 內(nèi)存地址
watchpoint set expression -w read -- 0x16b9dd91
18.3 條件斷點(diǎn)?watchpoint modify -c 表達(dá)式
例子:
watchpoint modify -c '*(int *)0x1457fa70 == 20'
命中后得到結(jié)果:
Watchpoint 3 hit:
old value: 15
new value: 20
19.找按鈕事件?po [按鈕名稱/內(nèi)存地址 allTargets]?
例子:
(lldb) po [[self btnTest] allTargets]
{(
??? <ViewController: 0x166af1f0>
)}
(lldb) po [[self btnTest] actionsForTarget:(id)0x166af1f0 forControlEvent:0]
<__NSArrayM 0x165b8950>(
testAction:
)
// 再來(lái)一發(fā),對(duì)按鈕屬性操作
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]
帶參數(shù)運(yùn)行:
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 讀取方式:從內(nèi)存+8的地方開始? 64bit自行變通
例:
常見(jiàn)問(wèn)題-打印無(wú)效
上面我們簡(jiǎn)單的學(xué)習(xí)了如何使用LLDB命令。但有時(shí)我們?cè)谑褂眠@些LLDB命令的時(shí)候,依然可能會(huì)遇到一些問(wèn)題。不明類型或者類型不匹配
p (void)NSLog(@"%@",[self.view viewWithTag:1001]) //記住要加void p (CGRect)[self.view frame] //記住不能寫成 self.view.frame,lldb的bug二、調(diào)試進(jìn)階
2.1.監(jiān)聽某個(gè)方法的調(diào)用
?
如果是自定義的view,比如QQView,想監(jiān)聽frame變化,直接[QQView setFrame:]即可 系統(tǒng)方法就要如圖所示,x86_64系統(tǒng)中,rdi表示第一個(gè)參數(shù),具體其他平臺(tái)可看inspecting-obj-c-parameters-in-gdb,里面有詳細(xì)的說(shuō)明
2.2.image尋址,找到崩潰行
此時(shí)會(huì)調(diào)用如下代碼會(huì)崩潰
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此時(shí)我們要直接找到崩潰的行數(shù),很簡(jiǎn)單。先找到非系統(tǒng)bug的崩潰地址,如下圖所示,顯然是第三行QQLLDB,右邊對(duì)應(yīng)的地址是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:602.3.crash日志分析,讀取符號(hào)表
1.要知道如何讀取符號(hào)表,我們得先偽造一份符號(hào)表的數(shù)據(jù)文件出來(lái),代碼如下
NSArray *arr=[[NSArray alloc] initWithObjects:@"1",@"2", nil]; NSLog(@"%@",arr[2]); 偽造步驟:
1.編譯到真機(jī)
2.然后進(jìn)入xcode將這個(gè)打開,找到QQLLDB.app.dSYM這個(gè)文件,偷偷拷貝一份到桌面
3.然后在product中clean一下工程
4.在真機(jī)上打開一個(gè)編譯進(jìn)去的程序,會(huì)出現(xiàn)圖
步驟如下圖所示:
?
?
之后在命令行操作
2.4觀察實(shí)例變量的變化
假設(shè)你有一個(gè) UIView,不知道為什么它的 _layer 實(shí)例變量被重寫了 (糟糕)。因?yàn)橛锌赡懿⒉簧婕暗椒椒?#xff0c;我們不能使用符號(hào)斷點(diǎn)。相反的,我們想監(jiān)視什么時(shí)候這個(gè)地址被寫入。
首先,我們需要找到 _layer 這個(gè)變量在對(duì)象上的相對(duì)位置:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar*)class_getInstanceVariable([MyView class], "_layer")) (ptrdiff_t) $0 = 8現(xiàn)在我們知道 ($myView + 8) 是被寫入的內(nèi)存地址:
(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就可以運(yùn)行調(diào)試了
2.基本命令
2.1.預(yù)覽圖片
UIImage *image = [UIImage imageNamed:@"clear"]; UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; [viewB addSubview:imageView];//此時(shí)執(zhí)行命令之后,圖片會(huì)被蘋果用自帶的預(yù)覽工具顯示出來(lái) visualize image2.2.邊框/內(nèi)容著色
如下所示,打印得到imageView地址之后,然后用border命令將其邊框著色unborder取消著色
(lldb) p imageView (UIImageView *) $2 = 0x00007fb8c9b15910 (lldb) border 0x00007fb8c9b15910 -c green -w 2同理,mask是給內(nèi)容著色?unmask
mask imageView -c green2.3.關(guān)系鏈的繼承
(lldb) pclass image UIImage| NSObject2.4.打印所有屬性
`pinternals`這個(gè)命令就是打印出來(lái)的一個(gè)控件(id)類型的內(nèi)部結(jié)構(gòu),詳細(xì)到令人發(fā)指!甚至是你自定義的控件中的類型,譬如這個(gè)styleView就是我自定義的,內(nèi)部有個(gè)iconView的屬性,其中的值它也會(huì)打印出來(lái)。 (lldb) pinternals image (UIImage) $8 = { NSObject = {isa = UIImage } _imageRef = 0x00007fc1f3330780 _scale = 2 _imageFlags = {named = 1imageOrientation = 0cached = 0hasPattern = 0isCIImage = 0renderingMode = 0suppressesAccessibilityHairlineThickening = 0hasDecompressionInfo = 0} }2.5.bmessage
如果Chisel
ViewController沒(méi)有設(shè)置viewWillDisappear這個(gè)方法,此時(shí)我想用斷點(diǎn)斷下來(lái),可以這樣
(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 = 0x0000000101a0b566Breakpoints
BreakPoint分類
breakpoint也是有分類的,我這里的文章內(nèi)大致按使用的方式分為了
- Normal Breakpoint,
- Exception Breakpoint,
- OpenGL ES Error breakpoint,
- Symbolic Breakpoint,
- Test Failure Breakpoint,
- WatchPoints。
?
可以按具體的情景使用不同類型的breakpoint,解決問(wèn)題為根本。
Normal Breakpoint
添加普通斷點(diǎn)就不多說(shuō)了,在源代碼的右側(cè)點(diǎn)擊一下即可。或者,使用快捷鍵:command + \ 來(lái)添加和刪除。這兩種方式添加的breakpoints在Xcode上面是可以通過(guò)UI看到的。
還有可以通過(guò)下面兩個(gè)LLDB命令直接在運(yùn)行時(shí)添加斷點(diǎn),但是這種方式需要注意的是一方面無(wú)法通過(guò)UI直接看到斷點(diǎn),另外一方面只存在于本次運(yùn)行,下一次啟動(dòng)模擬器重新運(yùn)行的時(shí)候,這些斷點(diǎn)就不生效了。
如上圖,通過(guò)“br li”命令打印所有的breakpoint,可以看到一共有3個(gè)breakpoint,第一個(gè)是通過(guò)Xcode的UI添加的,后面兩個(gè)分別是通過(guò)下面兩個(gè)命令添加的:
“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
可以通過(guò)下圖中Xcode的UI添加Exception Breakpoint。有時(shí)候,比如數(shù)組越界或者設(shè)置一個(gè)空對(duì)象等問(wèn)題,都會(huì)拋出一個(gè)異常,但是這種類型的錯(cuò)誤非常難以定位,這個(gè)時(shí)候就可以使用Exception Breakpoint來(lái)進(jìn)行調(diào)試,在異常發(fā)生時(shí)可以捕捉到并停止程序的執(zhí)行。OC中的異常是一個(gè)常被忽略的地方,但實(shí)際上系統(tǒng)框架內(nèi)這個(gè)使用非常廣泛,大部分這種錯(cuò)誤信息,系統(tǒng)框架都會(huì)以異常的形式throw出來(lái),所以善用這種breakpoint的話,我們能大大減少查找錯(cuò)誤的時(shí)間。
例如,當(dāng)我們添加如下Exception Breakpoint之后(bt 命令后文中會(huì)講解,這個(gè)命令的作用是在斷點(diǎn)觸發(fā)時(shí),打印回調(diào)棧信息):
類似下面這樣的數(shù)組越界的問(wèn)題,我們可以很容易就定位到問(wèn)題所在,不用再毫無(wú)頭緒找來(lái)找去了:
當(dāng)斷點(diǎn)暫停執(zhí)行時(shí),我們可以通過(guò)Xcode的UI中查看調(diào)用棧信息:
或者查看bt命令打印的調(diào)用棧信息:
還有類似如下的錯(cuò)誤可以通過(guò)這種斷點(diǎn)很容易定位到:
,不過(guò)這種問(wèn)題,可以通過(guò)使用setValue:forKey:代替來(lái)避免。
OpenGL ES Error Breakpoint
同上圖中,在Xcode的breakpoint navigator的下部添加按鈕,選擇”Add OpenGL ES Error Breakpoint”即可。這個(gè)breakpoint主要是用來(lái)在OpenGL ES發(fā)生錯(cuò)誤時(shí)停止程序的運(yùn)行。
Symbolic Breakpoint
通過(guò)Xcode的UI添加symbolic breakpoint的方式同exception breakpoint,彈出框如下:
Symbolic breakpoints 在某個(gè)特定的函數(shù)或者方法開始執(zhí)行的時(shí)候,暫停程序的執(zhí)行,通過(guò)這種方式添加斷點(diǎn),我們就不需要知道在源文件中添加,也不需要知道斷點(diǎn)設(shè)置在文件的第幾行。
上圖中,最主要的設(shè)置是Symbol的內(nèi)容,可以有如下幾種:
- 1. A method name,方法名稱,例如 pathsMatchingExtensions: 這樣的方法名稱,會(huì)對(duì)所有類的這個(gè)方法都起作用。
- 2. A method of a particular class. 特定類的某個(gè)方法。例如 ,[SKLine drawHandlesInView],或者 people::Person::name()
- 3. A function name。函數(shù)名稱。例如 ,_objc_msgForward 這樣C函數(shù)。
另外,也可以通過(guò)命令行的方式添加 Symbolic breakpoints。對(duì)C函數(shù)添加斷點(diǎn):
對(duì)OC的方法添加斷點(diǎn):
常用的這個(gè)類型的斷點(diǎn)有,objc_exception_throw可以用來(lái)代替 Exception Breakpoint,還有一個(gè)-[NSObject doesNotRecognizeSelector:] 也比較常用,用于檢測(cè)方法調(diào)用失敗。
下斷:
breakpoint set -a 函數(shù)地址 ? --常規(guī)斷點(diǎn)
breakpoint set --func-regex 函數(shù)關(guān)鍵字 ? --飄云提示:這個(gè)非常有用!我也是最近才研究發(fā)現(xiàn)的-雖然官方文檔一直有,但是沒(méi)重視
這樣下斷的優(yōu)勢(shì):
比如再某動(dòng)態(tài)庫(kù)中有 testA函數(shù),那么常規(guī)做法是先 image list -o -f 查看模塊基址 然后 image lookup -r -n 函數(shù)關(guān)鍵字找到偏移 ? 然后再 br s -a 基址+偏移!
用上面這個(gè)命令下端就簡(jiǎn)潔方便了!!!lldb會(huì)自動(dòng)幫你下斷所有匹配特征字的斷點(diǎn),可以模糊匹配哦
再來(lái)一個(gè)對(duì)動(dòng)態(tài)庫(kù)函數(shù)下斷的:
breakpoint set --shlib foo.dylib --name foo
這個(gè)也非常有用,可以進(jìn)行斷點(diǎn)過(guò)程中的一些自動(dòng)化處理:
breakpoint command add 斷點(diǎn)序號(hào)
這個(gè)也非常有用,對(duì)C函數(shù)下斷非常好 / 貌似是模糊匹配
breakpoint set -F isTest?? / 可以簡(jiǎn)寫為 b isTest
Test Failure Breakpoint
通過(guò)Xcode的UI添加方法同上。這個(gè)類型的break point 會(huì)在 test assertion 失敗的時(shí)候暫停程序的執(zhí)行。
Watchpoints
Watuchpoints是一個(gè)用來(lái)監(jiān)聽變量的值的變化或者內(nèi)存地址的變化的工具,發(fā)生變化時(shí)會(huì)在debugger中觸發(fā)一個(gè)暫停。對(duì)于那些不知道如何準(zhǔn)確跟蹤的狀態(tài)問(wèn)題,可以利用這個(gè)工具來(lái)解決。要設(shè)置watchpoint的話,在程序運(yùn)行到stack frame包含有你想觀察的變量時(shí),讓debugger暫停運(yùn)行,這個(gè)時(shí)候變量在當(dāng)前stack frame的scope內(nèi),這個(gè)時(shí)候才能對(duì)該變量設(shè)置watchpoint。
你可以在Xcode的GUI中設(shè)置watchpoint,在xcode的 Variables View中,把你想觀察的變量保留出來(lái),然后右鍵設(shè)置“Watch XXX”。例如下圖,觀察self的title變量,點(diǎn)擊 Watch “_button1ClickCount” 即可。
命令行
或者也可以通過(guò)命令行來(lái)設(shè)置watchpoint:watch set variable _button1ClickCount,詳細(xì)命令可以參考:http://lldb.llvm.org/lldb-gdb.html,有好幾種命令可以達(dá)到同樣的效果。
上面是對(duì)變量進(jìn)行觀察,實(shí)際上我們可以對(duì)任意內(nèi)存地址進(jìn)行觀察,命令如下:watchpoint set expression — 0x123456,參考:http://stackoverflow.com/questions/21063995/watch-points-on-memory-address
需要注意的是,watchpoint是分類型的,包括read,write或者read_write類型,這個(gè)非常容易理解,在讀,寫或者讀寫變量或內(nèi)存的時(shí)候,watchpoint是否被觸發(fā)。read,write或read_write跟著-w參數(shù)后面表示類型。另外,命令行中,watchpoint還有一些簡(jiǎn)寫,set簡(jiǎn)寫為s,watch簡(jiǎn)寫為wa,variable簡(jiǎn)寫為v。
下面的示例是來(lái)自?http://www.dreamingwish.com/article/lldb-usage-a.html?網(wǎng)站的幾個(gè)命令:
第一個(gè)命令是監(jiān)聽_abc4變量的內(nèi)存地址write的變化,第二個(gè)是監(jiān)聽_abc4變量read的變化,第三個(gè)是監(jiān)聽_abc3變量read_write的變化。
需要注意的是,通過(guò)Xcode的GUI添加的watchpoint為默認(rèn)類型,即write類型,如果想要添加讀寫都watch的watchpoint,則只能通過(guò)命令行工具進(jìn)行添加了。
使用watchpoint modify -c ‘(XXX==XX)’,則修改watchpoint之后在某個(gè)值的時(shí)候才會(huì)監(jiān)聽。
編輯選項(xiàng)
BreakPoint Condition
當(dāng)我們通過(guò)Xcode對(duì)breakpoint進(jìn)行編輯時(shí),可以發(fā)現(xiàn)normal breakpoint和symbolic breakpoint都有一個(gè)”Condition”輸入選項(xiàng),這個(gè)的作用很容易理解,只有在設(shè)置的condition表達(dá)式為YES的情況下這些斷點(diǎn)才會(huì)起作用。
例如,下圖中的breakpoint在判斷字符串相等的時(shí)候才會(huì)停止運(yùn)行:
可以注意到這里使用stringWithUTF8Stirng:方法,原因在于lldb的expression parser有一個(gè)bug,不兼容非ASCII字符,需要處理一下才行,否則會(huì)報(bào)錯(cuò)“An Objective-C constant string's string initializer is not an array”,參考:http://stackoverflow.com/questions/17192505/error-in-breakpoint-condition
更加簡(jiǎn)單一些的例子就不說(shuō)了,比如 i == 99之類的簡(jiǎn)單比較,只要表達(dá)式的結(jié)果為BOOL類型即可。
Breakpoint Actions
可以看到上面的每種breakpoint編輯選項(xiàng)中基本上都有“Add Action”選項(xiàng),當(dāng)breakpoint被觸發(fā)時(shí),都首先會(huì)執(zhí)行我們?cè)O(shè)置的這些action,然后我們才能得到控制權(quán),即Xcode上面才會(huì)顯示程序停止執(zhí)行的UI。這個(gè)Action通過(guò)例子比較好理解,我們通過(guò)上面那個(gè)setObject:forKey:的異常來(lái)說(shuō)明。代碼如下:
設(shè)置Breakpoint:
可以看到上圖中,我們一共設(shè)置了3個(gè)action。第一個(gè)action,用來(lái)打印exception的詳細(xì)信息,用法參考:http://stackoverflow.com/questions/17238673/xcode-exception-breakpoint-doesnt-print-details-of-the-exception-being-thrown。
第二個(gè)action,我們使用shell命令“say”,讓電腦發(fā)聲,把一段文字讀出來(lái)。
第三個(gè)action,我們使用“bt”命令來(lái)打印調(diào)用棧信息
設(shè)置完成之后,當(dāng)異常發(fā)生時(shí),我們會(huì)聽到電腦發(fā)聲念上圖中的英文,然后在log中可以看到如下信息,第一行是Exception的描述信息,下面是調(diào)用堆棧:
Continuing after Evaluation
看一下breakpoint的編輯彈窗,我們可以發(fā)現(xiàn)有一個(gè) “Automatically continue after evaluation actions” checkbox選項(xiàng)。當(dāng)我們勾選這個(gè)checkbox之后,debugger會(huì)執(zhí)行breakpoint中添加的所有的actions,然后繼續(xù)執(zhí)行程序。對(duì)于我們來(lái)說(shuō),除了觸發(fā)一大堆command并且執(zhí)行時(shí)間很長(zhǎng)的情況之外,程序會(huì)很快跳過(guò)這個(gè)breakpoint,所以我們可能根本不會(huì)注意到這個(gè)breakpoint的存在。所以,這個(gè)選項(xiàng)的功能相當(dāng)于在執(zhí)行的最后一個(gè)action之后,直接輸入continue命令繼續(xù)執(zhí)行。
有了這個(gè)很強(qiáng)大的功能,我們可以直接通過(guò)breakpoints來(lái)單獨(dú)對(duì)我們的程序進(jìn)行修改。在某行代碼時(shí)停止執(zhí)行,使用”expression”命令來(lái)直接修改程序的某個(gè)變量設(shè)置直接修改UI,然后繼續(xù)執(zhí)行。expression / call 配合這個(gè)選項(xiàng)的時(shí)候,會(huì)非常強(qiáng)大,可以很方便實(shí)現(xiàn)很多很強(qiáng)大的功能。
例如,我們實(shí)現(xiàn)一個(gè)如下的功能,把tableview的第一個(gè)cell的selectBackgroundView的背景色改為紅色:
action的內(nèi)容為“expression [[cell selectedBackgroundView] setBackgroundColor:[UIColor redColor]]”,這里的表達(dá)式先不用關(guān)心,我們后面LLDB章節(jié)會(huì)講到,修改之后,當(dāng)我們點(diǎn)擊cell的時(shí)候,cell的背景就會(huì)如下圖一樣變紅:
使用這種方式,我們?cè)诓恍枰薷囊恍写a的情況下,只需要通過(guò)修改breakpoint,就可以實(shí)現(xiàn)對(duì)UI的各種調(diào)試效果。
?
前言
你是否嘔心瀝血的嘗試去理解代碼和打印出來(lái)的變量?jī)?nèi)容?
NSLog( whatIsInsideThisThing);或是漏過(guò)函數(shù)調(diào)用來(lái)就簡(jiǎn)化工程行為?
NSNumber *n = @7; // theFunctionThatShouldReallyBeCalled();或者短路的檢查邏輯?
if (1 || theBooleanAtStake) { ... }亦或者是函數(shù)的偽實(shí)現(xiàn)?
int calculateTheTrickyValue {return 9;/*Figure this out later.... }那是不是要不斷的重編譯,然后又開始新的輪回?
構(gòu)建軟件是復(fù)雜的而且BUG無(wú)處不藏。一個(gè)正常的修正過(guò)程是修改代碼,編譯,再次運(yùn)行,然后祈禱上帝。
似乎也不用墨守成規(guī)。你可以用調(diào)試器啊!假設(shè)你已經(jīng)知道怎么檢視變量值,這里有更多你需要掌握的東西。
這篇文章的目的是挑戰(zhàn)你的調(diào)試知識(shí),把你可能知道得基礎(chǔ)知識(shí)點(diǎn)解析的更透徹,然后向你展示了一系列有趣的栗子。開始吧!
LLDB
LLDB是個(gè)開源調(diào)試器,REPL特性,自帶C++以及Python插件。它與Xcode綁定并且駐在控制臺(tái)界面化于窗口的下端。
調(diào)試器允許你在一個(gè)特定執(zhí)行時(shí)刻暫停程序,檢視變量值,執(zhí)行自定義命令,以及按你認(rèn)為合適得步驟進(jìn)行程序步驟操控。(調(diào)試器主要功能戳這里)
你以前使用調(diào)試器的部分很可能僅限于Xcode的UI上打個(gè)斷點(diǎn)。但是這有些技巧,你可以做一些更酷比的事情。通過(guò)GDB與LLDB之間對(duì)比是針對(duì)所有支持的命令行的一個(gè)很好鳥瞰式的學(xué)習(xí)法,你還可能想要去安裝Chisel,一套開源的LLDB插件讓你的調(diào)試更加有趣。
與此同時(shí),讓我們開始如何使用調(diào)試器打印變量值的旅程吧。
基礎(chǔ)
這里有一個(gè)簡(jiǎn)單短小的程序來(lái)打印字符串。注意到斷點(diǎn)被添加到了第八行:
程序到此會(huì)停下來(lái)然后打開控制臺(tái),讓我們能與調(diào)試器進(jìn)行交互。此時(shí)我們應(yīng)該輸入什么呢?
幫助
最簡(jiǎn)單得命令是鍵入help,你可以獲取一個(gè)命令行列表。如果你忘記一個(gè)命令或者想知道該命令更細(xì)致的使用方法,那么你可以通過(guò)調(diào)用help <command>,比如help print或help thread。如果你甚至忘記了命令本身,你可以嘗試使用help help,但是如果你懂得足夠多,你可能已經(jīng)徹底不要這個(gè)命令了。
打印
打印值很容易,只要試著鍵入print命令:
LLDB實(shí)際上支持前綴命令判斷,所以你同樣可以使用prin,?pri或者p。但是你不能使用pr,因?yàn)長(zhǎng)LDB不能分辨出你是否是想執(zhí)行process命令。(吐槽幸好p沒(méi)有歧義,暴露屬性)
你同時(shí)也注意到了結(jié)果帶一個(gè)$0。實(shí)際上你可以用這個(gè)來(lái)引用變量!試著鍵入$0 + 7然后你會(huì)看到106。任何帶美元符號(hào)是LLDB的命名空間,其存在是為了為你提供幫助。
表達(dá)式
如果你想修改一個(gè)值?修改,你說(shuō)的算?好吧,修改!下面來(lái)一個(gè)簡(jiǎn)單得表達(dá)式命令行:
這并不修改調(diào)試器中的值。實(shí)際上修改的是程序中的值!如果你繼續(xù)程序,它很神奇地會(huì)打印出42紅氣球(上下文)。
從現(xiàn)在開始注意一點(diǎn),我們?yōu)榱朔奖阌胮與e代替print和expression。
什么是打印命令?
這里有一個(gè)有意思的表達(dá)式來(lái)考慮下:p count = 18。如果我們執(zhí)行命令然后打印count的內(nèi)容,我們會(huì)看到它確實(shí)相當(dāng)于執(zhí)行了表達(dá)式count = 18。
這兩者的區(qū)別是print命令不帶參數(shù),這點(diǎn)與expression不同。考慮e -h +17。在選擇是否要進(jìn)行輸入源為+17,帶-h標(biāo)志的操作,還是選擇是否要進(jìn)行計(jì)算區(qū)分17和h操作,在這兩個(gè)選擇上面是不明確的。調(diào)試器認(rèn)為連字符導(dǎo)致了混淆,你可能得不到想要的結(jié)果。
幸運(yùn)的是,這個(gè)解決方法十分簡(jiǎn)單。使用--來(lái)表示表示符號(hào)的結(jié)束以及輸入源的開始。此時(shí)如果你想要用-h標(biāo)志,你可以使用e -h -- +17,如果你想要進(jìn)行區(qū)分,則你可以執(zhí)行e -- -h +17。不帶標(biāo)志則是十分普通,它(e --)有一個(gè)別名print。
如果你鍵入help print并且往下拖拽,你會(huì)看到:
'print' is an abbreviation for 'expression --'.打印對(duì)象
如果我們嘗試鍵入
p objects那輸出會(huì)有點(diǎn)冗繁:
(NSString *) $7 = 0x0000000104da4040 @"red balloons"當(dāng)嘗試打印一個(gè)更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu)時(shí)候會(huì)情況會(huì)更糟:
(lldb) p @[ @"foo", @"bar" ](NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"好吧,我們想看下對(duì)象的description方法。我們需要告訴expression命令作為對(duì)象來(lái)打印這個(gè)結(jié)果,使用-O標(biāo)志(這不是0):
(lldb) e -O -- $8 <__NSArrayI 0x7fdb9b71b3e0>( foo, bar )很走運(yùn),e -O --也有別名,其別名為po,我們可以只要這樣使用:
(lldb) po $8 <__NSArrayI 0x7fdb9b71b3e0>( foo, bar ) (lldb) po @"lunar" lunar (lldb) p @"lunar" (NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"打印變量
print命令有許多種不同的格式可以由你來(lái)指定。它們以命令格式為print/<fmt>或者更簡(jiǎn)單p/<fmt>。接下來(lái)舉個(gè)栗子。
默認(rèn)的格式:
(lldb) p 16 1616進(jìn)制格式:
(lldb) p/x 16 0x10二進(jìn)制格式(t代表tow):
(lldb) p/t 16 0b00000000000000000000000000010000 (lldb) p/t (char)16 0b00010000你還可以使用p/c打印字符,或者是p/s打印一個(gè)非終止類型的字符串char *。完整列表戳這里。
變量
至此你可以打印對(duì)象跟簡(jiǎn)單得類型,并可以在調(diào)試器中使用expression命令更改它們的值,讓我們使用一些變量來(lái)減少我們輸入工作。你可以聲明一個(gè)變量C來(lái)表示int a = 0,同樣你可以在LLDB中做同樣的事情。然后,變量必須以美元符號(hào)作為開頭:
(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不能識(shí)別出所牽扯的變量類型。不時(shí)會(huì)遇到,我們可以給一點(diǎn)提示:
(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0] 'M' (lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0] 77變量特性讓調(diào)試器更容易被使用,你這么認(rèn)為嗎?
流程控制
你的程序會(huì)在你打上斷點(diǎn)的位置停下來(lái)。
此時(shí)你看到在調(diào)試工具欄有四個(gè)按鈕,通過(guò)使用它們你可以控制程序的執(zhí)行流程:
這四個(gè)按鈕從左到右依次為:繼續(xù),單步,跳入,跳出。
首先,繼續(xù)按鈕將會(huì)讓你得程序繼續(xù)正常執(zhí)行(可能一直運(yùn)行或者遇到下一個(gè)斷點(diǎn))。在LLDB中,你可以使用process continue來(lái)繼續(xù)執(zhí)行,別名為c。
其次,單步執(zhí)行將會(huì)將單行代碼當(dāng)做黑盒一樣執(zhí)行。如果那行你調(diào)用了函數(shù),那將不會(huì)進(jìn)入這個(gè)函數(shù),而是直接執(zhí)行這個(gè)函數(shù)后繼續(xù)運(yùn)行。LLDB中相對(duì)應(yīng)的命令是thread step-over,next,或者?n。
如果你想進(jìn)入一個(gè)函數(shù)調(diào)用來(lái)檢查調(diào)試該函數(shù)的執(zhí)行,你可以使用第三個(gè)按鈕,跳入,LLDB同樣提供了thread step-in,step, 和s。注意到next與step在當(dāng)前行代碼不涉及函數(shù)調(diào)用的時(shí)候效果是一樣的。
大部分知道c,n,s。但是還有第四個(gè)按鈕,跳出。如果你不小心跳入了一個(gè)函數(shù)而你本意是想跳過(guò)它,一般反應(yīng)是不斷的按n知道函數(shù)返回。跳出幫你節(jié)省時(shí)間。它會(huì)執(zhí)行到return語(yǔ)句(知道執(zhí)行了出棧操作),然后會(huì)停下來(lái)。
舉個(gè)栗子
來(lái)看下如下的代碼片段:
代碼停在斷點(diǎn),然后我們執(zhí)行如下的命令行:
p i n s p i finish p i frame info這里,frame info將會(huì)告訴你當(dāng)前行以及源文件是啥,可以通過(guò)鍵入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命令會(huì)讓程序運(yùn)行直到isEven()函數(shù)返回,然后馬上停止。但是請(qǐng)注意,17行已經(jīng)執(zhí)行完了。
線程返回
還有一個(gè)特別幫的功能是你在調(diào)試的時(shí)候可以用thread return來(lái)控制程序流程。它使用可選參數(shù),將這個(gè)參數(shù)載入寄存器,單后馬上執(zhí)行返回命令,然后函數(shù)出棧。這意味著剩下函數(shù)沒(méi)有被執(zhí)行。這樣因?yàn)锳RC的引用計(jì)數(shù)/記錄出現(xiàn)問(wèn)題,或者遺漏一些清除操作。但在一個(gè)函數(shù)的開頭執(zhí)行這個(gè)命令是一個(gè)非常棒得函數(shù)打樁并且反悔了一個(gè)偽結(jié)果。
讓我們來(lái)對(duì)上述相同的代碼段跑如下的指令:
p i s thread return NO n p even0 frame info在看答案之前鄉(xiāng)下結(jié)果,答案如下:
(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斷點(diǎn)
我們一直都使用斷點(diǎn)來(lái)讓程序停止,檢視當(dāng)前狀態(tài)從而捕獲BUG。但是如果我們轉(zhuǎn)變對(duì)斷點(diǎn)的理解,我們可以獲得更多可能。
A breakpoint allows you to instruct a program when to stop, and then allows the running of commands.
考慮在函數(shù)剛開始處打一個(gè)斷點(diǎn),使用thread return來(lái)重寫函數(shù)行為,然后繼續(xù)。現(xiàn)在想象下自動(dòng)實(shí)現(xiàn)這種處理。是不是聽起來(lái)很牛X,不是么?
斷點(diǎn)管理
Xcode提供了一套工具來(lái)創(chuàng)建和操作斷點(diǎn)。我們將會(huì)逐一過(guò)一遍并且進(jìn)行描述與之對(duì)應(yīng)的LLDB命令行。
在Xcode的左面板上,有一堆按鈕集合。有一個(gè)長(zhǎng)得很像斷點(diǎn)。點(diǎn)擊打開斷點(diǎn)導(dǎo)航欄,進(jìn)去之后你一眼看到你所操作的所有斷點(diǎn):
這里你可以看到所有的斷點(diǎn) - 對(duì)應(yīng)LLDB中的breakpoint list或者是br li。你可以點(diǎn)擊單個(gè)斷點(diǎn)進(jìn)行打開或者關(guān)閉 - 對(duì)應(yīng)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 = 11.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.創(chuàng)建斷點(diǎn)
(UI創(chuàng)建略了。。。是人都會(huì)吧。。)
在調(diào)試器中打斷點(diǎn),使用breakpoint set命令:
(lldb) breakpoint set -f main.m -l 16 Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x縮寫可以用br。b是另外一個(gè)完全不同的命令,是_regexp-break的別名,但是它足夠健壯來(lái)進(jìn)行創(chuàng)建上述命令一樣效果的斷點(diǎn):
(lldb) b main.m:17 Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x你也可以防止一個(gè)斷點(diǎn)在一個(gè)符號(hào)(C語(yǔ)言函數(shù)),而不用指定行數(shù):
(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現(xiàn)在這些斷點(diǎn)會(huì)停止正在將要執(zhí)行的函數(shù),同樣適用與OC方法:
(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 如果你想通過(guò)UI來(lái)創(chuàng)建象征性斷點(diǎn),你可以點(diǎn)擊左下端斷點(diǎn)導(dǎo)航欄的+號(hào):
然后選擇第三個(gè)選項(xiàng):
此時(shí)出現(xiàn)彈出框讓你輸入比如-[NSArray objectAtIndex:]的符號(hào),然后程序在這個(gè)函數(shù)調(diào)用的時(shí)候便可以停止下來(lái),不管是你的代碼或者還是大蘋果的代碼!
如果我們看下其他選項(xiàng),我們可以發(fā)現(xiàn)一些有意思的選項(xiàng),同樣提供了各種條件觸發(fā)的鍛煉只要你點(diǎn)擊了Xcode的UI并且選擇了“Edit Breakpoint”選項(xiàng):
如上圖,斷點(diǎn)只有在i為99的時(shí)候才會(huì)停止程序。你可以同樣設(shè)置“ ignore”選項(xiàng)來(lái)告訴斷點(diǎn)在前n次調(diào)用的時(shí)候不用停止程序(條件為真)。
這里還有一個(gè)“Add Action”按鈕。。。
斷點(diǎn)動(dòng)作
可能上面斷點(diǎn)的栗子中,你想知道每次斷點(diǎn)時(shí)候i值是多少。我們可以使用動(dòng)作p i,然后當(dāng)斷點(diǎn)觸發(fā)的時(shí)候我們進(jìn)入調(diào)試器,它會(huì)預(yù)先執(zhí)行這個(gè)命令在將控制流程交給你之前:
你也可以加多重動(dòng)作,可以是調(diào)試器指令,shell指令或者更健壯的打印信息:
如上你可以看到打印出i值,還有強(qiáng)調(diào)語(yǔ)句,打印出自定義的表達(dá)式。
下面是上述功能用純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 = 0Breakpoint commands:p i Condition: i == 991.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0自動(dòng)化,我們來(lái)了!
計(jì)算值之后繼續(xù)
如果視線停留在斷點(diǎn)彈出框的底端,你會(huì)額外看到一個(gè)選項(xiàng):“Automatically continue after evaluation actions(計(jì)算動(dòng)作后自動(dòng)執(zhí)行)。”它只是一個(gè)勾選框,但是它卻有強(qiáng)大的能力。如果你勾選上了,調(diào)試器將會(huì)蘋果你所有的命令然后繼續(xù)執(zhí)行程序。表面上看上跟斷點(diǎn)沒(méi)有打住一樣(除非你斷點(diǎn)太多了,拖慢了程序進(jìn)度)。
這個(gè)勾選框功能與最后一個(gè)動(dòng)作斷點(diǎn)繼續(xù)執(zhí)行效果一樣,但是有勾選框更加容易點(diǎn)。對(duì)應(yīng)調(diào)試器的指令如下:
(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 = 0Breakpoint commands:continue1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0計(jì)算后自動(dòng)繼續(xù)運(yùn)行讓你可以單獨(dú)通過(guò)使用斷點(diǎn)來(lái)修改你的程序!你可以停止在單行,運(yùn)行一個(gè)expression命令來(lái)改變變量,然后繼續(xù)。
舉個(gè)栗子
考慮下簡(jiǎn)陋?dú)埧岬摹按蛴∈秸{(diào)試”技術(shù)。不是用:
NSLog( whatIsInsideThisThing);而是用斷點(diǎn)處設(shè)置打印變量值替代吊打印日志打印語(yǔ)句然后繼續(xù)。
不是用:
int calculateTheTrickyValue {return 9;/*Figure this out later....*/ }而是用斷點(diǎn)處調(diào)用thread return 9然后繼續(xù)執(zhí)行。
帶動(dòng)作的象征斷點(diǎn)確實(shí)真的很強(qiáng)大。你也可以添加這些斷點(diǎn)到你朋友的Xcode工程并且讓動(dòng)作將所有信息細(xì)致展示出來(lái)。接下來(lái)看看要耗時(shí)多久來(lái)進(jìn)行計(jì)算以及會(huì)發(fā)生什么吧。
調(diào)試器完整操作
在起舞之前還有一點(diǎn)需要我們注意。你真的可以在調(diào)試器中執(zhí)行任何的C/OC/C++/Swift命令。比較弱的是我們不能創(chuàng)建一個(gè)新的函數(shù)。。。這意味著沒(méi)有新的類,塊,函數(shù),帶虛方法的C++類等等。除了這個(gè),調(diào)試器什么都能滿足!
我們可以分配一些字節(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ù)組的4個(gè)字節(jié):
(lldb) x/4c $str 0x7fd04a900040: monk我們還可以后三個(gè)字節(jié):
(lldb) x/1w `$str + 3` 0x7fd04a900043: keys當(dāng)你所要的活結(jié)束的時(shí)候別忘記了釋放內(nèi)存避免造成內(nèi)存泄露:
(lldb) e (void)free($str)跳舞吧,騷年!
現(xiàn)在我們已經(jīng)清楚基礎(chǔ)步驟,是時(shí)候來(lái)整一些比較瘋狂的東西了。我過(guò)去曾寫過(guò)一篇博客(大家自己收藏。。。)發(fā)表在looking at the internals of NSArray。當(dāng)時(shí)用了大量的NSLog語(yǔ)句,后來(lái)全用調(diào)試器搞定了。它是一個(gè)很好的調(diào)試器使用練習(xí)。
暢通無(wú)阻(無(wú)斷點(diǎn)模式)
當(dāng)你的應(yīng)用在跑的時(shí)候,Xcode中的調(diào)試工具欄展示一個(gè)停止按鈕而非繼續(xù)狀態(tài)的按鈕:
選中這個(gè)按鈕的時(shí)候,應(yīng)用遇到斷點(diǎn)將會(huì)停止(就像輸入了process interrupt)。這時(shí)候?qū)?huì)讓你進(jìn)入調(diào)試器。
這里有一個(gè)有趣的地方。如果你運(yùn)行一個(gè)iOS應(yīng)用,你可以嘗試這個(gè)(全局變量可提供)
(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>>可以看到整個(gè)層級(jí)!Chisel(上文提及)用pviews來(lái)實(shí)現(xiàn)。
更新UI
然后,通過(guò)上述的輸出,我們可以看到隱藏的視圖:
(lldb) e id $myView = (id)0x7f82b1d01fd0然后在調(diào)試器中修改它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]在你下次繼續(xù)運(yùn)行這個(gè)程序的時(shí)候你才會(huì)看到變化。這因?yàn)檫@個(gè)變化需要傳遞給渲染服務(wù)然后視圖展示才會(huì)被更新。
渲染服務(wù)實(shí)際上是另一個(gè)進(jìn)程(稱作后臺(tái)),并且甚至我們調(diào)試進(jìn)程被停止了,這個(gè)后臺(tái)也不會(huì)被停止!
這意味著不通過(guò)繼續(xù),你可以執(zhí)行:
(lldb) e (void)[CATransaction flush]在模擬器中或者設(shè)備中的UI會(huì)進(jìn)行刷新而你還在調(diào)試器中!Chisel提供了一個(gè)別名函數(shù)叫做caflush,并且它被用來(lái)實(shí)現(xiàn)其它捷徑像hide <view>,show <view>還有其他許多許多。所有的Chisel命令都有對(duì)應(yīng)的文檔,所以就在安裝它之后鍵入help來(lái)隨心所欲的獲取更多的信息吧。
壓入視圖控制器
想象一個(gè)簡(jiǎn)單的應(yīng)用有一個(gè)UINavigationController作為根視圖控制器。你可以在調(diào)試器中相當(dāng)簡(jiǎn)易的執(zhí)行如下操作:
(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]然后壓入子視圖控制器:
(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]最后執(zhí)行:
(lldb) caflush // e (void)[CATransaction flush]你會(huì)看到馬上壓入了一個(gè)視圖控制器。
找到按鈕的目標(biāo)
想象下你調(diào)試器中有一個(gè)變量,$myButton,你想要去創(chuàng)建它,并從UI中抓取它,或者簡(jiǎn)單地只是你想在斷點(diǎn)停下來(lái)的時(shí)候?qū)⑺鳛閭€(gè)局部變量。你可能想知道當(dāng)你點(diǎn)擊它的時(shí)候是誰(shuí)接收了這個(gè)動(dòng)作。這里展示達(dá)到這點(diǎn)有多么的簡(jiǎn)單:
(lldb) po [$myButton allTargets] {(<MagicEventListener: 0x7fb58bd2e240> )} (lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0] <__NSArrayM 0x7fb58bd2aa40>( _handleTap: )現(xiàn)在你可能想在事件發(fā)生的時(shí)候添加一個(gè)斷點(diǎn)。只要在LLDB或者Xcode設(shè)置象征性斷點(diǎn)在-[MyEventListener _handleTap:]。and you are all set to go!
觀察實(shí)例變量值變化
想象一個(gè)假設(shè)的場(chǎng)景你有一個(gè)UIView且它的_layer實(shí)例變量被重寫了。因?yàn)檫@里可能不涉及方法,我們不能使用象征性斷點(diǎn)。取而代之的是我們想觀察一個(gè)內(nèi)存地址什么時(shí)候被寫入了。
首先我們需要找到_layer對(duì)象在那里:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer")) (ptrdiff_t) $0 = 8現(xiàn)在我們知道($myView + 8)這個(gè)內(nèi)存地址被寫入了:
(lldb) watchpoint set expression -- (int *)$myView + 8 Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = wnew value: 0x0000000000000000對(duì)應(yīng)Chisel里面的wivar $myView _layer。
在非重寫方法上的象征性斷點(diǎn)
想象你想知道什么時(shí)候-[MyViewController viewDidAppear:]被調(diào)用了。如果MyViewController實(shí)際上沒(méi)有實(shí)現(xiàn)這個(gè)方法,但是父類實(shí)現(xiàn)了呢?我們可以設(shè)置一個(gè)斷點(diǎn)來(lái)看看具體情況:
(lldb) b -[MyViewController viewDidAppear:] Breakpoint 1: no locations (pending). WARNING: Unable to resolve breakpoint to any actual locations.因?yàn)長(zhǎng)LDB根據(jù)符號(hào)搜索,它找不到該方法,所以你的斷點(diǎn)將不會(huì)被觸發(fā)。你所需要做的是設(shè)置一個(gè)條件,[self isKindofClass:[MyViewController class]],然后見(jiàn)這個(gè)斷點(diǎn)設(shè)在UIViewController上。一般來(lái)說(shuō),設(shè)置一個(gè)這樣的條件是有效的,但是,這里無(wú)效是因?yàn)槲覀儧](méi)有父類該方法的實(shí)現(xiàn)。
viewDidAppear:是大蘋果寫的,所以沒(méi)有對(duì)應(yīng)的符號(hào);在方法內(nèi)部也沒(méi)有self。如果你想要使用在象征性斷點(diǎn)內(nèi)使用self,你需要知道它在那里(可能在寄存器也可能在棧上;在x86你可能在$esp+4找到它)。這是個(gè)通過(guò)的歷程,因?yàn)槟阒酪呀?jīng)知道有四種體系架構(gòu)了。吐槽略。。幸運(yùn)的是,Chisel已經(jīng)完成了這些封裝,你可以調(diào)用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 = 0x000000010e11533cLLDB與Python
LLDB有完整的內(nèi)置Python支持。如果你在LLDB上輸入腳本,它會(huì)打開一個(gè)Python REPL。如果你在LLDB中鍵入script,它會(huì)打開一個(gè)Python REPL。你可以傳入一行Python語(yǔ)句到script命令來(lái)不進(jìn)入REPL的情況下進(jìn)行執(zhí)行腳本:
(lldb) script import os (lldb) script os.system("open http://www.objc.io/")這允許你創(chuàng)建各種各樣的酷比命令。將這個(gè)丟入文件,~/myCommands.py:
def caflushCommand(debugger, command, result, internal_dict):debugger.HandleCommand("e (void)[CATransaction flush]")然后在LLDB中運(yùn)行如下:
command script import ~/myCommands.py或者,將這行代碼放置于/.lldbinit讓LLDB每次運(yùn)行的時(shí)候都執(zhí)行一次。Chisel不過(guò)就是一堆Python腳本用來(lái)組合字符串,然后告訴LLDB來(lái)執(zhí)行這些字符串。聽起來(lái)很簡(jiǎn)單吧!呃?
參考鏈接:
1.當(dāng)異常出現(xiàn)時(shí)
2.日志記錄CocoaLumberjack
3.在Xcode中調(diào)試程序
4.南峰子的技術(shù)博客
5.與調(diào)試器共舞 - LLDB 的華爾茲
6.官方調(diào)試技巧文檔
7.inspecting-obj-c-parameters-in-gdb
總結(jié)
以上是生活随笔為你收集整理的Xcode高级调试技巧(1)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ps调色滤镜库:ON1 Effects
- 下一篇: 配电自动控制系统