0%

iOS开发中一些Debug方法

前言


当时刚开始搞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键,在出现的弹出视图上面,点击像眼睛一样的图标,你可以看到:

Image1

那么图片就显示出来了,并且你还可以选择 Open With Preview 在预览中打开该图片。当然还有一种方法能做到,在控制台的左侧,当断点执行时,你会看到相关对象,选择你要查看的对象,现在我也要查看UIImage对象,然后按space键,也能将图片显示出来。

Image2

刚刚上面说的两种方法,除了能显示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文件viewDidLoad34行。

一些问题的处理


在使用以上一些方法的时候,也遇到了一些问题。有的时候,它找不到对象类型或者方法,比如下面这个:

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)

这个是找不到UIViewframe属性,我们可以制定输出的类型,或者引入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 的华尔兹