Xcode常见的程序崩溃及其调试

来源:互联网 发布:sybase数据库日志查看 编辑:程序博客网 时间:2024/05/28 22:13

                               Xcode常见的程序崩溃及其调试


这个画面是不是很熟悉?

两种常见crash:SIGABRT以及EXC_BAD_ACCESS

SIGABRT是可控的crash,因为系统知道应用出现了一些不该出现的错误比如


这些信息中有一些给出了错误的线索,比如


 “unrecognized selector sent to instance XXX” 这样的错误意味着,程序调用了不存在的方法,即某个对象没有该方法,而你却用该方法向其发送消息。

EXC_BAD_ACCESS,调试起来比较困难,由于内存管理问题,app出现了一些错误的状态。

首先得知道问题出在哪,为了解决这个问题,你可以使用调用栈(也叫栈轨迹或者回溯)。当程序崩掉时,会出现如下图,Xcode(会自动切换到Debug导航栏)


如图,它会显示所有处于活动状态的线程,在崩掉的线程处会有高亮显示,如上图的14 main。通常情况下它会在线程1(Thread1 程序的主线程)崩掉,因为大部分工作都是在主线程上完成的,当然,如果你用到队列或者后台线程,App同样会在其他线程崩掉。

如上图,高亮显示在main.m里面的main函数,但是main函数没有提供有用的信息,所以我们需要进一步挖掘出有用的信息。你可以滑到Debug导航栏的底部,将底部的滑杆推到最右边。它会显示崩溃时的调用栈。如下图所示


调用栈将会显示当前处于活动状态的函数或者方法,由于崩溃,调试器(debugger)暂停了程序,相应得所有的方法和函数也就被冻结了。底部的start方法(main下面)首先被调用,它会调用main函数,即一个应用的入口函数,main函数通常位于调用栈的底部。Main函数会调用UIApplicationMain方法,也就是在main函数中被高亮显示的那一行即


继续来看调用栈,UIApplicationMain调用_run,紧接着_run调用CFRunLoopRunSpecific(),如此按照调用栈顺序调用,直到进行到__pthread_kill函数结束。


以上调用栈中的方法(或函数)除了main以外,都是ios框架内置的,是看不到源码的。

所以你点击其中的任何方法,出现的都是一些栈踪迹,而非源代码。


看到这些栈踪迹,是不是感觉很困惑?

下面我们来解读一下栈踪迹,选择任何一个main以外的函数

Exception Breakpoint


当程序crash掉时,都会抛出异常,抛出异常的原因是程序做了一些不应该做的事。所以肯定想知道程序是在哪里抛出异常的,这时我们可以利用Exception BreakPoint来处理。利用它,我们可以让Xcode在抛出异常的位置处暂停

设置exception breakpoint的方法如下

 

 

在这个窗口底部有个“+”点击之后,会自动添加一个exception breakpoint。


现在再run,现在它会在异常抛出处停下来,并在这一行高亮显示


根据console我们可以知道,我们向viewcontroller发送了一个并不属于他的方法,这时我们就可以检查出自己的错误所在,然后进一步完善自己的程序。

小窍门:当你看到“unrecognizedselector sent to instance XXX”这种错误时,通常是给某个对象实例发送了一个本不属于它的方法的消息

可能的原因:1.方法名拼写错误

                        2.该方法并不属于该对象

内存管理错误(EXC_BAD_ACCESS)

关于内存管理的crash,很难精确找出错误所在,因为它可能很早就已经发生了,发生内存泄露时,他不会马上把这个错误暴露出来,这是和SIGABRT最大区别所在。

发生这种错误可能的原因:

1.      初始化数组或者字典时没有在末尾处添加nil,导致编译器不知道你的目的。

错误:NSArray *array = [NSArray arrayWithObjects:@“1”,@“2”];

正确:NSArray *array = [NSArray arrayWithObjects:@“1”,@“2”,nil];

2.      本来已经释放的对象,却再向它发送消息,如:

[a release];    //假设此时retaincount为0

[a someMethod];//此时就会crash

  

修改后,再run,会发现程序还是crash掉了(当然不是因为前两个错误),最终停止在main函数里,这时肯定有疑惑,第一步不是已经设置好了exception breakpoint了吗,为什么没有在异常抛出点停下来而在main里面停下来呢?Bingo,下面继续。

 

如果你从上往下浏览调用栈,你会发现有一些是和NSObject and Key-Value Coding相关的。然后就是UIRuntimeOutletConnectionconnect,看起来像是outlet连接有错,然后下面是加载xib时出现的问题,这样我们就得到了一些有用的线索。在console里面没有出现有用的信息,这时我们有两种方法来解决

1.如下图

(也可以在console中输入c命令 c=continue效果是一样的)

然后在console里面会出现相应的错误信息


图中下半部分的16进制数可以忽略,那是一些调用栈的相关信息,注意上面两行,可以知道抛出的异常为NSUnknownKeyException问题发生在MainViewController这个类中的setValue:forUndefinedKey方法,因为Mainviewcontroller这个类是没有button这个属性(property)。所以在.h中添加一个名为”button”的输出口


小窍门:“this class is not key valuecoding-compliant for the key XXX”类型的错误通常发生在加载xib文件时,由于不存在相关的属性名。所以在代码中移除输出口属性时,同样在xib文件中移除相应的连接。

2.

在console中输入如下命令:po $eax   这个好处时,程序还是停留在那里没有像方法1那样被继续执行。 $eax 代表Cpu的寄存器,这个寄存器有一个指向NSException对象的指针。$eax只适用于模拟器调试,当真机调试时,可以使用$r0

输入该命令后,回车,将会有相关的异常描述(description)

 

断言失败(Assertion Failures)

断言是内部兼容的检测异常抛出的工具。

断言错误:

 

我们可以在自己的代码中添加断言来检测异常。如:

上面方法不允许传入的string变量为空或者长度小于3,只要任意一个条件不满足,则程序异常终止并会抛出异常。断言的好处是,他只会出现在调试模式下,对最终发布时的应用没有影响。

从调用栈中我们可以看出,问题出现在table view的重绘。这时我们可以用内存管理错误中给出的两种方法来进行调试。

 

当程序crash并抛出EXC_BAD_ACCESS错误时,我们可以使用下面的方法来找出错误所在。

点击Stop右边会弹出如下对话框

再进行如下设置

在把程序run一遍,在console(调试窗口)得到如下crash信息

勾选上Enable Zombie Objects,当你release某个对象时(若释放之后retaincount为0),但是为它分配的内存没有被deallocate(释放),相反,它会被标记为”undead”。如果之后你想再调用这块内存,程序会识别出这个错误然后终止并抛出“message sent to deallocated instance”这么一个错误。根据相应的错误信息,可以猜测出自己程序中大概那些地方会存在一些隐藏的bug。

综合上面所说,可以总结出一些调试方法

  • 如果程序crash在main.m中,可以设置Exception Breakpoint.
  • 如果设置了Exception Breakpoint.也没用的话可以尝试继续执行或者在lldb中使用po $eax命令.
  • 如果是EXC_BAD_ACCESS错误,则设置Zombie Objects然后在运行程序.
  • 最常见的crash或者其他bug原因是没有为xib或者storyboard连接输出口.这些通常不会导致编译器错误,因此可能无法察觉。
  • 不要忽略编译器的警告,最好解决掉所有编译器给出的警告。
  • 在真机和模拟器上调试得到的结果可能有些差别,因为二者的环境不尽相同。比如内存:模拟器的内存是电脑的内存,而真机的内存通常是比模拟器要小很多的。
  • 最后,作为新手最好开启静态分析工具,它可以捕获到更多的错误信息,可以在build settings里面来设置。

      本文Demo     

     参考资料:Ray Wenderlich


尊重他人劳动成果,转载请注明出处。谢谢!

原创粉丝点击