前言
当时刚开始搞iOS开发的时候,对断点调试的理解局限于:看代码风骚的走位,即当运行出来的效果对不住我的代码时,我会去看代码是怎么运行的,然后用雍正之剑去砍杀八阿哥。后面慢慢的接触lldb后,发现lldb用起来真的很赞,下面我来分享一下我在平时开发中积累的一些用法,这些用法有些是从网上发现的,有些是同事告诉我的。
相关用法
打印网络请求的相关信息 现在开发项目中没有网络请求都不好意思说自己在搞项目了,那为了方便调试,是不是要把请求的网址、请求的参数、服务器返回的数据等相关打印出来呢?我以前的做法都是在每个请求的API方法里将这些信息NSLog打印出来,直到最近我的主管告诉了我一个秘密。在网络请求库收到data的方法里面添加Debugger Command类型的断点。
1 2 3 po [connection.currentRequest.URL absoluteString] po [NSString stringWithCString:(char*)[connection.currentRequest.HTTPBody bytes] encoding:4] po [NSString stringWithCString:(char*)[data bytes] encoding:4]
其实上面打印的东西我们都知道,我只是把他们放在了一个断点里面,让可以进行三连击:1、输出请求网址;2、输出请求参数;3、输出返回数据。
这样不仅解决了NSLog打印出来的中文数据显示为UTF8格式,还能省下我们很多代码量,是不是很nice。。。
注:AF接收数据的方法 在AFURLConnectionOperation文件
1 2 - (void)connection:(NSURLConnection __unused *)connection didReceiveData:(NSData *)data)
显示图片 当你创建UIImage对象从服务器端获取数据时,假如中间出了一点小问题,为了验证图片是否创建成功,可以试一下下面的debug方法。
用将鼠标放在UIImage对象上面,然后按option键,在出现的弹出视图上面,点击像眼睛一样的图标,你可以看到:
那么图片就显示出来了,并且你还可以选择 Open With Preview 在预览中打开该图片。当然还有一种方法能做到,在控制台的左侧,当断点执行时,你会看到相关对象,选择你要查看的对象,现在我也要查看UIImage对象,然后按space键,也能将图片显示出来。
刚刚上面说的两种方法,除了能显示UIImage对象以后,UIImageView、UIView也行,至于其他的类型,我以前没有操作过,在以后的实践中可以试试。
po 的一个隐藏指令 在lldb中,po
(print object
的缩写) 是打印某个对象的指令,自然的 po xxxx (某个对象)应该是我用的最多的指令了,其实还有一个指令用来查看某个View的层级结构关系 po [self.view recursiveDescription]
。或许你觉得在控制栏下输出一大串代码不是很直观,那么你可以试试Xcode6 出来的 View debug方法,它能很好的解决这个问题,能更直观的查看视图的层级结构。
改变某些值 有的时候,我们程序运行时改变某个值,来看看效果。我们可以用exp
(expression
的缩写)。
改变某个字符串的值?
1 2 3 4 5 6 7 8 9 10 11 12 13 (lldb) expression NSString *$xxString = @"111" (lldb) po $xxString 111 (lldb) po $xxString = @"2222" 2222 (lldb) expression $xxString = @"3333" (__NSCFString *) $2 = 0x00007fc793675a10 @"3333" (lldb) po $xxString 3333 (lldb)
哦哦,对了 exp Class *$instance
是在lldb中创建实例。
1 2 3 4 5 6 7 8 9 10 (lldb) expression int $b = 10 (lldb) po $b 10 (lldb) exp $b = 100 (int) $0 = 100 (lldb) po $b 100 (lldb)
当然,我们还可以改变某个对象的属性,比如,如果我想要改变一个视图的背景颜色。
1 2 3 po self.view.backgroundColor = [UIColor redColor] call imageView.backgroundColor = [UIColor greenColor]
*有时候,我想需要改变某个函数的返回值,来测试函数的稳健性,那可以试试 thread return XXX
指令。 * 看一下下面的代码吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /// Value NSString *string = [self exeGetReturnString]; /// Method - (NSString *)exeGetReturnString { return @"1111"; } /// lldb (lldb) thread return @"This is Changed String!!!" (lldb) po string This is Changed String!!! (lldb)
我把断点设置在函数exeGetReturnString
的返回值前面,可以看出string
的值变了。这些改变值的方法,有时在调试程序的时候是非常有用的。
指定输出的格式
对于基本简单类型,我们可以指定它的输出类型p/x
(x表示输出的类型),这种最能体现在的就是整数类型数据之间二进制、八进制、十进制、十六进制的转换,虽然我们能算出来,但电脑应该算的比我们快,比我们准确吧!好吧,那让我们来一发试试吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 (lldb) expression int $a = 1 (lldb) po $a 1 (lldb) p/t $a (int) $a = 0b00000000000000000000000000000001 (lldb) p/o $a (int) $a = 01 (lldb) p/x $a (int) $a = 0x00000001 (lldb) p/d 0b00000000000000000000000000000001 (int) $0 = 1 (lldb) p/d 01 (int) $1 = 1 (lldb) p/d 0x00000001 (int) $2 = 1 (lldb)
上面用的p/X
(X代表进制,t,二进制;o,八进制;d,十进制;x,十六进制
)。
上面输出的 $X(0-2)
,这是啥意思呢?其实可以将他们看作是对操作对象的一个引用,可以直接使用这个符号来操作相对应的对象。
1 2 3 4 5 6 7 8 (lldb) po $0 1 (lldb) po $1 1 (lldb) po $2 1
查找奔溃信息的位置
程序崩溃了要咋办?找到崩溃的地方,解决它撒。我们可以添加一个全局的断点Add Exception Breakpoint
,当程序崩溃的时候,它能定位错误的代码位置。但是还有一种方法能找到错误的位置。image lookup --address 0xXXXXXXXX
(0xXXXXXXXX为程序奔溃时控制台给出的地址)。
1 2 NSString *originalString = @"0123"; NSString *subString = [originalString substringFromIndex:5];
好吧,这个代码的问题比较简单,我只是举个例子哈,[emoji]。
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 2015-05-17 23:26:04.790 DebugMethod[5282:1628084] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSCFConstantString substringFromIndex:]: Index 5 out of bounds; string length 4' *** First throw call stack: ( 0 CoreFoundation 0x0000000104a70c65 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x0000000104707bb7 objc_exception_throw + 45 2 CoreFoundation 0x0000000104a70b9d +[NSException raise:format:] + 205 3 Foundation 0x0000000104276ca8 -[NSString substringFromIndex:] + 118 4 DebugMethod 0x00000001041d3936 -[ViewController viewDidLoad] + 710 5 UIKit 0x0000000104f9b210 -[UIViewController loadViewIfRequired] + 738 6 UIKit 0x0000000104f9b40e -[UIViewController view] + 27 7 UIKit 0x0000000104eb62c9 -[UIWindow addRootViewControllerViewIfPossible] + 58 8 UIKit 0x0000000104eb668f -[UIWindow _setHidden:forced:] + 247 9 UIKit 0x0000000104ec2e21 -[UIWindow makeKeyAndVisible] + 42 10 UIKit 0x0000000104e66457 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 2732 11 UIKit 0x0000000104e691de -[UIApplication _runWithMainScene:transitionContext:completion:] + 1349 12 UIKit 0x0000000104e680d5 -[UIApplication workspaceDidEndTransaction:] + 179 13 FrontBoardServices 0x000000010766f5e5 __31-[FBSSerialQueue performAsync:]_block_invoke_2 + 21 14 CoreFoundation 0x00000001049a441c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12 15 CoreFoundation 0x000000010499a165 __CFRunLoopDoBlocks + 341 16 CoreFoundation 0x0000000104999f25 __CFRunLoopRun + 2389 17 CoreFoundation 0x0000000104999366 CFRunLoopRunSpecific + 470 18 UIKit 0x0000000104e67b42 -[UIApplication _run] + 413 19 UIKit 0x0000000104e6a900 UIApplicationMain + 1282 20 DebugMethod 0x00000001041d3d3f main + 111 21 libdyld.dylib 0x000000010703f145 start + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException
其实从上面的崩溃信息中,我们可以看出是越界NSRangeException
的问题,原因是'*** -[__NSCFConstantString substringFromIndex:]: Index 5 out of bounds; string length 4'
,字符串本来长度为4,而要取的位置为5,所以越界了!!!
假如,如果你一个ViewController里面用到了多个[NSString substringFromIndex:]
方法,那是不是比较难找到崩溃的地方?这样image lookup --address 0xXXXXXXXX
就派上用场了,从上面的错误信息中,可以初步断定viewDidLoad
方法里的substringFromIndex:
方法出错了,那我们就拿viewDidLoad
的里面的错误地址取寻找。
1 2 3 4 (lldb) image lookup --address 0x00000001041d3936 Address: DebugMethod[0x0000000100001936] (DebugMethod.__TEXT.__text + 710) Summary: DebugMethod`-[ViewController viewDidLoad] + 710 at ViewController.m:34 (lldb)
结果还真的被猜中了,从上面的结果来看,代码错在ViewController.m
文件viewDidLoad
34行。
一些问题的处理
在使用以上一些方法的时候,也遇到了一些问题。有的时候,它找不到对象类型或者方法,比如下面这个:
1 2 3 4 5 6 7 8 9 10 11 12 13 (lldb) po self.view.frame error: property 'frame' not found on object of type 'UIView *' error: 1 errors parsing expression (lldb) po (CGRect)self.view.frame error: 'CGRect' is not a valid command. (lldb) po (CGRect)[self.view frame] (origin = (x = 0, y = 0), size = (width = 375, height = 667)) (origin = (x = 0, y = 0), size = (width = 375, height = 667)) (lldb) exp @import UIKit (lldb) po self.view.frame (origin = (x = 0, y = 0), size = (width = 375, height = 667)) (origin = (x = 0, y = 0), size = (width = 375, height = 667)) (lldb)
这个是找不到UIView
的frame
属性,我们可以制定输出的类型,或者引入UIKit
库。
1 2 3 4 5 6 7 8 (lldb) expr NSDictionary *$tmpDict = [dataDict[@"Data"] firstObject] error: no known method '-firstObject'; cast the message send to the method's return type error: 1 errors parsing expression (lldb) expr NSDictionary *$tmpDict = (NSDictionary *)[dataDict[@"Data"] firstObject] (lldb) po $tmpDict { Key = 1; }
这个是找不到-firstObject
这个方法。
总结
以上这些我提到的方法,只是调试中的冰山一角,lldb里还有很多宝藏去挖掘。在开发项目中,对我们开发有利的去多了解,至于其他更牛逼的技巧(与实际开发项目没有多大关系的),我们有时间、有精力、有想法的可以多去探讨。研究的越多、越广,你会发现自己有很多很多很多很多都不知道,[emoji],发现自己很菜很菜,因为搞技术就像一个无底洞嘛。
参考
About LLDB and Xcode
工具篇:LLDB调试器
与调试器共舞 - LLDB 的华尔兹