这篇文章的作者是iOS Tutorial Team 的成员,他是一個经验丰富的ios设计开发工作者他的联系方式: 和
我们大多数开发人员经常遇到这样的情况:我们的应用运行的好好的,突然——“砰”┅下子crash了抓狂!
假如此时很郁闷的你立即开始尝试修改代码,并期望如同寻找到了合适的咒语一样使得bug神奇消失那么很可能导致更糟糕的问题。但是如果掌握了系统的定位crash问题的方法的话解决crash问题也不是很复杂的事情。
首要的事情是找到代码中crash出现的确切位置:哪個文件的哪行代码。本文将全面的阐述如何利用Xcode的调试工具定位代码的奔溃位置
本文面向所有的开发者,从初级到高级即便是高级开發者,也可能通过本文获得一些调试技巧或者以前不曾涉及的调试知识
下载.这是个有bug的程序。用Xcode打开可以看到有至少八处编译警告,通常编译警告也是问题的前期表现本文中用的Xcode4.3来做说明,但实际上在4.2的版本也是一样的
注: 本文的示例程序效果是在IOS5的模拟器上运行嘚效果,如果直接在手机上运行同样也会crash,但是crash出现的顺序可能会有所不同
在模拟器上运行看看发生了什么情况
SIGABRT的Crash通常情况下好定位佷多,因为它是受控的crash(系统让app去执行某个app本身并不支持的操作时应用终端就会直接抛出该信号,让程序crash)
EXC_BAD_ACCESS的crash定位就要难很多。这种crash經常发生在应用进入了一个损坏状态时通常是由内存管理问题导致。
幸运的是我们上面的第一个crash(目前暴露出来的)是SIGABRT。SIGABRT的crash发生时┅般在Xcode的调试输出窗口(窗口的右下角)会有错误信息输出。如果你看不到调试输出窗口通过View—》Debug Area-》Show Debug Area显示调试输出区域。此处这个crash的錯误信息如下:
学会解析这些错误信息很关键。因为这些信息中往往包含了很重要的错误原因描述比如上面输出中比较有趣的一行:
知噵了crash的原因就好了,当前的首要任务就是找出代码中出错的地方必须找到源文件名字以及出错行数。可以借助调用堆栈(also known as the stacktrace or the backtrace)
当一个应鼡crash,Xcode窗口左侧的区域会切换进入调试导航页显示出当前应用中活动状态的线程信息,并高亮标出crash掉的线程 通常都是应用的主线程Tread 1,因為大部分的业务工作在这个线程中完成的如果应用中使用了队列或后台线程,崩溃也会出现在这些线程中
目前,Xcode会自动标出出错点在main.m攵件中的main()函数中此处并不会提供更多信息,所以我们必须更深入一些寻找线索
查看更多的堆栈信息,往右侧拖动堆栈信息下方的滑块这样将会完整显示当前crash掉的线程信息:
列表中的每一条都是一个应用中或者某一个IOS frameworks的函数或方法。堆栈信息能显示出应用中当前还处于活动状态的方法和函数调试器暂停了应用中断了这些函数和方法的执行。
最后一条函数start()是入口这个函数执行的过程中调用了它上面的函数, main()。是应用的入口点经常显示在靠近底部的位置。main() 调用了 UIApplicationMain()就是编辑窗口中绿色箭头指向的代码行。
除了main()调用堆栈中所有的函数和方法全部是灰化显示。这是因为这些信息都来自编译好的iOS库没有有效的源代码导致。
堆栈中的源代码文件只有main.m所以尽管main.m并不是真正引叺crash的文件,但是Xcode文件编辑器提示崩溃的点还是在这个源文件里这个经常弄迷糊新手,所以本文中将快速给出一个方便大家理解的途径
點击堆栈信息中其他的任何一条信息,都能看到一堆毫无头绪的汇编代码:
哦如果有源代码就好了! :-)
因此到底该如何找到crash的代码行呢?不论何时出现了上面的堆栈信息的时候都是app抛出了异常。(也可以说是堆栈上调用到了objc_exception_rethrow函数)应用做了不该做的事情就会抛出异常目前关注的是这个异常导致的结果:app做了些不应该做的事情,抛出了异常Xcode将其呈现出来。我们想确切知道到底是哪里抛的异常
幸运的昰,Xcode还可以打全局断点暂停程序断点可以帮助开发人员在某个场景暂停程序执行,本文的第二部分将会详细阐述这块这里需要使用的昰一种特殊的断点——全局断点,程序crash前进入全局断点
可以进入断点导航页设置全局断点:
新的全局断点就添加好了:
点击确定按钮退出彈框提醒。此时Xcode的工具栏的断点按钮现在变成可用状态如果你想程序跑起来不进任何断点的话,可以点击这个断点按钮关闭断点但是現在,打开断点并运行app
好了,代码编辑器现在不再是不再是汇编代码而是指向了源代码,同时注意看左侧的堆栈信息(是否切换出来堆栈信息由Xcode的设置决定)也发生了变化。
注: 一旦出现 “unrecognized selector sent to instance XXX” 错误, 首先检查对象的类型是不是正确以及被调用的方法究竟是否存在经常遇箌的情况是指针并没有指向正确的值导致实际调用的压根就是预期外的其他类型对象的方法。
另外方法名拼写错误也会导致出现该问题。稍后将会给出一个这样的示例程序
三、你的第一个内存错误
第一个问题修复了,再来运行应用看看 喔,又在同一行Crash了, 只不过这次是 EXC_BAD_ACCESS 錯误看样子,该应用还有内存处理上的问题
定位内存相关的crash相对来说会比较困难,因为隐患可能出现在崩溃前已经运行很久的其他代碼中有问题的代码破坏了内存结构,并不会立即在程序中体现出来而是到一段时间后,其他地方再访问这段内存有问题了才会爆出crash絀来。
而且事实上可能测试的时候这种bug根本就不会暴露出来,最终往往暴露在用户的手机上谁也不想出现这种情况。然而这种类型的crash吔是比较容易处理的如果仔细查看代码编辑框就会发现,Xcode原来早就警告我们这些存在内存处理不妥当的代码行了看见代码左侧行标上嘚黄色三角形了么?那就是一个编译警告点击黄色的三角形,Xcode会自动弹出一个“Fix-it”的修复建议如下图:
此处用一种对象的序列来初始囮NSArray对象,这种序列需要以nil结尾但是此处并没有以nil结尾,编译器不知道该何处是序列的结尾所以就报了这个警告出来。运行时因为没囿明显的结尾标志,所以系统会在读完了所有的参数后还尝试获取并不存在的对象,添加到序列中所以就崩溃了。
这种错误实在不应該犯特别是Xcode已经给出警告后。修复这个bug可以像下面代码一样给序列添加nil结尾项(或者,直接点击“Fix-it”):
四、“这个类不符合键值编碼”
在运行代码看还有哪些其他有趣的bug。但是你是怎么知道的它又一次崩溃在了main.m中。尽管全局断点还有效我们却看不到任何高亮的代碼提示这次代码的crash实实在在没有发生在我们应用的源代码中。堆栈信息证实除了main(),没有一个方法属于我们的应用:
outlets)有关系.再往下调用嘚方法是从nib中加载view这已经给出了线索。然而Xcode的调试窗还没有很方便定位的错误信息因为系统尚未抛出异常。全局断点仅仅会在程序告訴你异常原因前中断程序有时候你会获取到一个明确的错误信息,有时候并不能获取到
有时候可能需要多点几下,才能看到打印出来嘚错误信息:
和之前一样忽略下方的数字它们表示的是调用栈信息,但是我们已经有关于它们的更方便和可读的信息了!就是左侧的调試导航页面
异常的名字NSUnknownKeyException通常能很好的指示出问题原因。比如此处就告诉我们代码某处使用了系统不知道的“键值(unknown key)”。这里的某处佷明显是MainViewController而且键值名应该就是“button”。
可以确定问题就发生在加载nib的时候。虽然应用直接使用的是storyboard但是更深入些storyboard实际就是所有nib的集合,所以问题应该就处在storyboard中
在链接监测区,可以看到试图控制器中心的UIButton被链接到MainViewController的“button”了所以storyboard/nib有一个出口叫“button”,但是根据错误信息看嘚话实际根本没有这个出口。
此处@property 定义了名为 “button” 出口, 所以到底是怎么回事? 如果你注意到编译警告或许就不难找到问题症结了。
代码並没有准确的@synthesize按钮属性它告诉MainViewController有一个名字叫“button”的属性,但是却并没有提供实例变量以及存取方法(这些都是由@synthesize完成的)
现在运行程序将不洅crash了!
现在应用可以运行了或者说至少启动没问题了,来点击按钮试试。
哇哦应用崩溃在main.m中,还报了个SIGABRT错误信息调试窗口的错误信息如下:
堆栈信息并不很明了,它列出了所有的可能通过这样那样途径发消息或执行操作的方法但是已经可以知道哪个操作有问题。畢竟这里是点击了UIButton调用IBAction method时出的问题。
当然之前已经遇到过类似问题了。因为调用了一个并没有实现的方法导致不过这次目标对象MainViewController似乎没有问题,因为活动方法就是在这个有按钮的view controller中而且头文件MainViewController.h中也确实存在IBAction方法:
或者是不是这样?错误信息是想告诉我们方法名是buttonTapped泹是MainViewController的方法名却是以冒号结尾的buttonTapped:,因为它允许传入一个参数(名字叫“sender”)反过来说,错误信息中的方法名并不包含冒号因为不需要传入參数。所以正确格式的方法应该是这样:
这里到底是怎么回事呢方法初始化的时候是没有参数的格式(有些情况允许没有参数的响应方法),同时storyboard将该方法关联成了按钮的点击(Touch Up Inside)事件响应。但是后来方法变成了包含一个“sender”参数的格式,但是storyboard的关联没有实时更新
峩们可以在storyboard的按钮链接窗口看到如下场景:
先断开点击(Touch Up Inside)的链接(点击小的X按钮),接着将其再一次链接到主视图控制器不过这次选择buttonTapped:方法。注意这时链接窗口中的方法名末尾是包含了冒号的。
再运行程序后点击按钮什么鬼?尽管现在已经使用了有冒号的正确格式的点擊方法buttonTapped:还是报“unrecognized selector”错误信息,
如果仔细查看的话就会发现编译警告又是跟上面类似的场景。Xcode在抱怨MainViewController实现文件不完整特别是buttonTapped:没有实现。
好了这个很好修复修改下名字:
请注意,尽管将方法定义成IBAction会使代码看上去整洁但是也没有必要非将方法定义成IBAction。
注:如果留神编译器嘚警告的话这章节的问题都是比较容易定位的。就个人来说我是将所有的警告都当做错误来处理 (在Xcode的编译设置页:Build Settings screen有一个设置项,将警告当做error) 所以我会在程序运行前处理修复掉所有的警告信息。 Xcode能很好的指出如上的低级错误留神这些警告信息能达到事半功倍的效果。
继续之前的操作:运行程序点击按钮,等待崩溃是呢,不负所望:
好惊讶这次是EXC_BAD_ACCESS错误中的另一种。幸运的是Xcode告诉我们崩溃发生嘚位置在buttonTapped:方法中这一行:
有时候,这种问题会让我们反应不过来发生了什么同样不用担心,Xcode提供了帮手点击黄色的三角形看看有什么錯误:
NSLog()使用的是Objective-C类型的字符串,并不是C类型的字符串所以插入一个@来修复:
仔细看发现黄色警告信息并没有消除。因为这行还有其他不知道会不会导致崩溃的问题存在这同样很有趣,有时候代码运行的好好的或者说看上去执行的好好的,但是其他不知道什么时候它就崩溃了(当然此类型的崩溃经常发生在客户手机上)
让我们来看看具体的警告信息:
%s表示的是C类型的字符串。C类型的字符串实际只是一串连续嘚以空字符(NULL character实际值是0)结尾的byte类型数组。比如C类型的字符串“Crash!”实际在内存中的存储如下:
如果你的方法或函数中用到了C类型的字符串,那必须先确认字符串是以0结尾的否则函数处理时没办法识别出字符串的结尾。
现在当你在NSLog()格式的字符串或者NSString的stringWithFormat的字符串中使用%s,参數就会被当做C类型的字符串解析这种情况下,作为参数传入的“sender”实际是一个UIButton的对象并不是个C类型的字符串。甚至当“sender”指向的内存Φ包含字节0NSLog()将不会崩溃,而是输出类似如下的信息:
可以准确的看到这些信息来自何处再一次运行程序,点击按钮等待崩溃在左侧嘚调试区,右击“sender”选择“View Memory of *sender”项(确认点击的是带*号的)
Xcode这时会这块内存地址存储的内容,也就是刚刚NSLog()输出的信息
然而并不能保证这塊内存中一定有NULL字节,所以EXC_BAD_ACCESS错误并不是能轻易报出的如果一直在模拟器上运行程序,可能跑很长时间都出不来这个错误因为你自己的測试环境总是你自己最喜欢的状态。这也导致此类型的bug很难浮现
当然,这种情况发生时Xcode已经给你了类型错误的警告了所以这种特殊的bug吔是很好查找的。但是不论何时只要使用了C类型的字符串或者直接操作了内存,都要特别小心是不是影响了其他地方的内存导致它们絀了问题。
如果应用总是奔溃那么恭喜你这个bug定位起来实际很容易,但是比较常见的是应用程序时而崩溃时而不崩溃,这将是问题复現非常困难!这种情况下定位这个bug也将变成史诗级的难题
修复此处NSLog()语句的问题,用下面的方法:
运行程序再一次点击按钮NSLog()做了我们期朢它做的事情,但是似乎我们还没有处理好buttonTapped:的崩溃
Xcode告诉我们这个最新的crash在这一行:
调试窗口没有信息输出。你可以像之前一样一直点击繼续运行按钮或者你也可以在调试器中输入一个命令去获取错误信息。这样做的好处是程序还是中断在之前的位置。
如果是在模拟器仩运行可以输入如下的(lldb)命令:
LLDB是Xcode4.3及以上版本中默认调试器。如果使用的是早期的Xcode版本可以使用GDB调试器。他们俩的基本命令一致所以即便你的Xcode编译命令前面的标记是(gdb)而不是上面的(lldb),也一样可以继续(顺便补充一下you can object)。参数$eax指向一个CPU寄存器出现异常的情况下,这个寄存器中的数据将包含一个指向NSException对象的指针注意:$eax仅在模拟器环境下有效,如果你使用真机调试那么要访问的寄存器是$r0。
数字并不重要但是明显你正在处理的NSException对象是存储在这里的。
你可以通过这个对象调用任何NSException的方法比如:
这行命令将输出该异常的名称,比如这里是NSInvalidArgumentException另外输出如下命令:
将告诉我们异常的原因:
注: 调用“po $eax”的时候, 通常也会调用到对象的“description”方法并且输出, 这种情况下一般就已经给出了错誤消息。
正好就解释了刚刚发生了什么:你的本意是执行一个名叫“ModalSegue”的segue但是显然MainViewController中不存在这个东东。
storyboard显示一个segue使用的是模态方式但昰你忘记设置它的标识,这是个典型的错误:
修改segue的标识为“ModalSegue”再一次运行程序,点击按钮等待crash。这次不再崩溃了!但是呢这里遗留了我们下一篇要讨论的问题:显示的tableview不应该为空!
所以这个空白的tableview到底是关于什么的? 这是本文的一个悬念,下一篇中会详细解释同时吔会解决一些很有趣的在你编程生涯中曾经遇到过bug。当然通过第二部分学习你还可以更加充实自己的调试技能,比如NSLog()语句断点以及僵屍对象(Zombie Objects)。
当所有的这些做完讲完我承诺程序一定会按照我们期待的那样运行!最重要的是,你已经掌握了技能当你的程序出现这些囹人挫败的问题时你也必将能妥善处理它们。
有任何意见和建议请至论坛找我!
这篇文章的作者是iOS Tutorial Team 的成员,他是一个经验丰富的ios设计开发笁作者他的联系方式: 和