在开发App的过程中,需要反复的修改和优化我们的代码,特别是当程序出现bug,需要快速的找出错误的原因,进行修改,以保证程序的正确执行。
断点调试,是最常用最简单的一种调试方式,可以一步步跟踪程序执行的流程,得到变量的值,快速的找到错误的原因。
1、断点的基本操作
一,断点的设置
断点的操作非常简单,下面通过几张图来介绍一下如何添加、删除、编辑以及使用断点。设置断点也可以通过快捷键(command+"\")在需要设置断点的代码处
二,断点的自定义
(1)在你设置断点的地方,右击该断点,会弹出一个栏,选择Edit Breakpoint,可以对断点进行自定义设置
(2)点击Edit Breakpoint选项后,弹出如下图
蓝色对勾:蓝色对勾后面表示,当前断点所处的位置。将对勾抹去,表示该断点失效。(代码行数显示你可以在菜单栏Xcode->Preferences->TestEditing->勾选Line Numbers,将代码行数字显示出来。)
Condition:指的是条件表达式,该项允许我们对断点生效设置条件,表示当满足某一特定条件的前提下,该断点才生效。(该条件的录入,不能够识别预处理的宏定义,也不能识别断点作用域之外的变量和方法)。
Ignore:忽略次数。它指定了在断点生效,应用暂停之前,代码忽略断点的次数。你如果希望应用运行一段时间后断点才生效,那么就可以使用这个选项。比如说在调试某一循环体的时候。
Action:动作。它表示当断点生效时,Xcode作出反应后的行为动作。点击右边的Add Action选项会弹出如图下菜单。
图中所示红色方框中的选项,可以让你指定那一种动作。默认的是Debugger Command。还有以下几种动作供选择,下面逐一介绍。
1.AppleScript
它是苹果提供的一种脚本语言,用来执行一些预先指定的行为。选中该选项,将会出现如下图所示的AppleScript语言的输入框。
我在输入框中输入了相关的打印信息,它的意思是弹出一个显示“Hello World!”的对话框。点击Compile按钮后,如果没有错误,会显示成功信息。而点击Test按钮,会测试运行效果,如下图
至于红色方框中的内容是三种特殊符号相对应的定义。
符号标记 | 定义 |
---|---|
@expression@ | LLDB表达式 |
%B | 断点的名称 |
%H | 遇到该断点的次数 |
这个功能用于当断点生效时,捕获GPU当前所绘制的帧。该功能是辅助图形调试的。
3.Debugger Command
默认的选项,可以让断点执行LLDB调试命令。
4.Log Message
使用Log命令可以生成消息队列,将相关的消息输出到控制台上,还有一个Speak Message选项,可以播报消息。
5.Shell Command
该动作接收一个命令文件和参数列表。如下图
命令文件必须是一个可执行的二进制程序或者脚本。可以复制粘贴输入路径,也可以点击Choose按钮选择具体文件。
参数通过空格表示分割,也可以在两个@字符之间包含LLDB表达式。
一般情况下,Xcode会异步执行Shell Command,也就是说,Shell Command 和调试器将会同步执行。如果希望调试器在Shell Command命令完成后运行,则可以勾选下面的Wait until done选项。
6.Sound
动作会在断点被触发时,弹出声音提示。
2、全局断点
设置全局断点(异常断点),当遇到错误,Debug程序会自动定位到栈底信息,即跳到出错代码所在行。
Exception:选项可以让你选择响应Objective-C对象抛出的异常,也可以选择响应C++对象抛出的异常。
Break:则是选择断点所接收的异常,是接收“Throw”语句抛出的异常还是Catch语句的。
3、条件断点
设置条件断点,当满足条件的时候,才触发断点,适合用于循环结构中,可以准确的定位到某次循环。
4、符号断点
符号断点可以中断指定函数的调用,也可以定位到出现异常的代码处,并打印异常信息。
一,设置方法
二,通过 ObjC_exception_throw 定位到异常抛出的位置
三,unrecognized selector send to instancd 快速定位
在Symbolic中填写如下方法签名 -[NSObject(NSObject) doesNotRecognizeSelector:]
四,添加指定的方法为断点,比如添加一个 viewDidLoad Symbol
如果你要添加某个特定类的实例方法,可以用 -[类名 实例方法名]。类方法是+[类名 方法名]
5.OpenGL ES错误断点(OpenGL ES Error BreakPoint)
6、NSLog输出
在调试的过程中,经常通过NSLog在控制台输出需要的信息。NSLog输出比较消耗系统资源,输出的数据也可能会暴露出App里的保密信息,所在在发布正式版本之前必须把所有的NSLog输出都屏蔽掉。
NSLog除了输出基本信息,对于结构体也可以一次性输出。
7、动态输出(lldb)
在程序的调试过程中,除了通过断点调试在Xcode下方查看变量的值,也可以通过NSLog在控制台输出想要的信息。使用NSLog输出需要在运行前就把想要输出的信息写好,如果有改变,需要重新编译运行,效率非常低。
LLDB 是一个有着 REPL 的特性和 C++ ,Python 插件的开源调试器。LLDB 绑定在 Xcode 内部,存在于主窗口底部的控制台中。调试器允许你在程序运行的特定时暂停它,你可以查看变量的值,执行自定的指令,并且按照你所认为合适的步骤来操作程序的进展。
GDB to LLDB 参考是一个非常好的调试器可用命令的总览。 当然你也可以通过在输入台,键入Help命令,查看相关的调试命令
Help
它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过 help <command> 来了解更多细节,例如 help print 或者 help thread。如果你甚至忘记了help 命令是做什么的,你可以试试 help help。
例如:
lldb help
lldb help print
就是打印值 LLDB 实际上会作前缀匹配。所以你也可以使用 prin,pri,或者 p。但你不能使用 pr,因为 LLDB 不能消除和 process 的歧义
你可能还注意到了,结果中有个 $0。实际上你可以使用它来指向这个结果。试试 print $0 + 7,你会看到 106。任何以美元符开头的东西都是存在于 LLDB 的命名 空间的,它们是为了帮助你进行调试而存在的。
1>print ,po 与expression的关系
考虑一个有意思的表达式:p count = 18。如果我们运行这条命令,然后打印 count 的内容。我们将看到它的结果与expression count = 18 一样。
和 expression 不同的是,print 命令不需要参数。比如 e -h +17 中,你很难区分到底是以-h 为标识,仅仅执行+17 呢,还是要计算17 和 h 的差值。连字符号确实很让人困惑,你或许得不到自己想要的结果。
幸运的是,解决方案很简单。用 -- 来表征标识的结束,以及输入的开始。如果想要-h 作为标识,就用e -h -- +17,如果想计算它们的差值,就使用e -- -h +17。因为一般来说不使用标识的情况比较多,所以 e -- 就有了一个简写的方式,那就是print。
输入 help print,然后向下滚动,你会发现:
'print' is an abbreviation for 'expression --'.
(print是 `expression --` 的缩写)
打印对象
尝试输入
p objects
输出会有点啰嗦
(NSString *) $7 = 0x0000000104da4040 @"red balloons"
如果我们尝试打印结构更复杂的对象,结果甚至会更糟
(lldb) p @[ @"foo", @"bar" ]
(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"
实际上,我们想看的是对象的 description 方法的结果。我么需要使用 -O (字母 O,而不是数字 0) 标志告诉 expression 命令以 对象 (Object) 的方式来打印结果。
(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
幸运的是,e -o -- 有也有个别名,那就是po (print object 的缩写),我们可以使用它来进行简化:
(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
2>打印变量
可以给 print 指定不同的打印格式。它们都是以 print/<fmt> 或者简化的 p/<fmt> 格式书写。下面是一些例子:
默认的格式
(lldb) p 16
16
十六进制:
(lldb) p/x 16
0x10
二进制 (t 代表 two):
(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000
你也可以使用 p/c 打印字符,或者 p/s 打印以空终止的字符串 (译者注:以 '\0' 结尾的字符串)。
打印对象(po)
(lldb) po anObj
(lldb) po 0x0715aa40
po用于打印输出对象信息。使用动态指令需要与断点配合使用,这样就能够动态的打印输出程序执行到某个断点时的信息。
打印表达式(expr)
expo:可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。LLDB 实际上会作前缀匹配。所以你也可以使用 e
(lldb) expr 5+2
(lldb) e aString = @"aNewValue"
打印调用堆栈(bt)
打印调用堆栈,加all可打印所有thread的堆栈。 查询地址(image)
image 命令可用于寻址,有多个组合命令。比较实用的用法是用于寻找栈地址对应的代码位置。 下面我写了一段代码
NSArray *arr=[[NSArray alloc] initWithObjects:@"1",@"2", nil];
NSLog(@"%@",arr[2]);
这段代码有明显的错误,程序运行这段代码后会抛出下面的异常:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:(
0 CoreFoundation 0x0000000101951495 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x00000001016b099e objc_exception_throw + 43
2 CoreFoundation 0x0000000101909e3f -[__NSArrayI objectAtIndex:] + 175
3 ControlStyleDemo 0x0000000100004af8 -[RootViewController viewDidLoad] + 312
4 UIKit 0x000000010035359e -[UIViewController loadViewIfRequired] + 562
5 UIKit 0x0000000100353777 -[UIViewController view] + 29
6 UIKit 0x000000010029396b -[UIWindow addRootViewControllerViewIfPossible] + 58
7 UIKit 0x0000000100293c70 -[UIWindow _setHidden:forced:] + 282
8 UIKit 0x000000010029cffa -[UIWindow makeKeyAndVisible] + 51
9 ControlStyleDemo 0x00000001000045e0 -[AppDelegate application:didFinishLaunchingWithOptions:] + 672
10 UIKit 0x00000001002583d9 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 264
11 UIKit 0x0000000100258be1 -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1605
12 UIKit 0x000000010025ca0c -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 660
13 UIKit 0x000000010026dd4c -[UIApplication handleEvent:withNewEvent:] + 3189
14 UIKit 0x000000010026e216 -[UIApplication sendEvent:] + 79
15 UIKit 0x000000010025e086 _UIApplicationHandleEvent + 578
16 GraphicsServices 0x0000000103aca71a _PurpleEventCallback + 762
17 GraphicsServices 0x0000000103aca1e1 PurpleEventCallback + 35
18 CoreFoundation 0x00000001018d3679 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 41
19 CoreFoundation 0x00000001018d344e __CFRunLoopDoSource1 + 478
20 CoreFoundation 0x00000001018fc903 __CFRunLoopRun + 1939
21 CoreFoundation 0x00000001018fbd83 CFRunLoopRunSpecific + 467
22 UIKit 0x000000010025c2e1 -[UIApplication _run] + 609
23 UIKit 0x000000010025de33 UIApplicationMain + 1010
24 ControlStyleDemo 0x0000000100006b73 main + 115
25 libdyld.dylib 0x0000000101fe95fd start + 1
26 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
现在,我们怀疑出错的地址是0x0000000100004af8(可以根据执行文件名判断,或者最小的栈地址)。为了进一步精确定位,我们可以输入以下的命令:
image lookup -- address 0x0000000100004af8
命令执行后返回:
Address: ControlStyleDemo[0x0000000100004af8] (ControlStyleDemo.__TEXT.__text + 13288)
Summary: ControlStyleDemo`-[RootViewController viewDidLoad] + 312 at RootViewController.m:53
我们可以看到,出错的位置是RootViewController.m的第53行。
详情请考参考这里
8、僵尸模式
1. 什么是 EXC_BAD_ACCESS?
简单的说就是,你向一个已经释放的对象发送消息。在c和Objective-c中,我们对内存的操作都是通过指针完成的,不是直接对内存进行操作。指针中存储的是内存地址,通过内存地址,我们就能对该存储区域进行操作。但是,当该存储器区域不再映射到您的应用时,或者换句话说,该内存区域在你认为使用的时候却没有使用,该内存区域是无法访问的。 这时内核会抛出一个异常( EXC ),表明你的应用程序不能访问该存储器区域(BAD ACCESS)。一般情况下,EXC_BAD_ACCESS是由被损坏的指针引起的。比如野指针,内存泄漏等等。
2.为什么要了解EXC_BAD_ACCESS
Xcode可以把那些已经release掉得对象,变成“僵尸”,当我们访问一个Zombie对象时,Xcode可以告诉我们正在访问的对象是一个不应该存在的对象了,你的应用程序将会由于EXC_BAD_ACCESS而崩溃。因为Xcode知道这个对象是什么,所以可以让我们知道这个对象在哪里,以及这是什么时候发生的。而Xcode这种能力就是通过僵尸模式来体现的。
3.僵尸调试模式
单击左上角的Edit Scheme,并选中Edit Scheme。
在左侧选中Run ,在上方打开 Diagnostics选项。要启用僵尸对象,勾选 Enable Zombie Objects选框。
如果你现在遇到EXC_BAD_ACCESS ,在Xcode的控制台输出,告诉你该从哪里查找问题。看看下面的例子输出。
2016-07-28 06:31:55.501 Debug[2371:1379247] -[ChildViewController respondsToSelector:] message sent to deallocated instance 0x17579780
这条消息对于定位问题有很好的提示作用。但是很多时候,只有这条提示是不够的,我们需要更多的提示来帮助定位问题,这时候再加入 MallocStackLogging 来启用malloc记录。