我发现在ios讨论群里(欢迎喜欢交流和热心的iphone开发朋友加入qq群参与讨论:186739796,验证码:csdn。)很多人会贴一些系统在控制台输出的崩溃代码,来咨询是什么问题导致的。其实问题真的是没有范式的,但是解决问题的方法却大多相通。我以前也遇到过很多的crash,也是从慢慢的解决问题当中走过来的。我大致的收集了一些关于调试的技巧的文章,希望大家能体会出解决问题背后的一些思考方法,这才是快速解决问题的最好的利器!
part-1
转自:http://article.ityran.com/archives/1006
有这样一种情形:当我们正在快乐的致力于我们的app时,并且什么看都是无比顺利,但是突然,坑爹啊,它崩溃了。(悲伤地音乐响起)
我们需要做的第一件事就是:不要惊慌。
修复崩溃不是很困难的。假如你崩溃了,并且胡乱的改些东西,而且还在不停的念着咒语希望bug神奇的自动消失,你大多数情况下都会使情况更麻烦。相反的,你需要知道一些系统的方法,并且学习怎么找到崩溃和他的原因。
第一件需要知道的就是在你的代码中准确的找到crash发生的地方:在那个文件,那一行。Xcode debugger将会帮助你,但是你需要懂得怎么样最好的使用它,这也是这篇教程展示给你的。
这篇教程对于所有的开发者都是有利的。即使你是一个很有经验的ios开发者,你也可能会从中学习到一些你不知道的小窍门。
准备开始
下载这个例子程序。你将会看到这是一个有bug的程序。当你打开这个项目的时候,xcode会显示至少8个编译警告,这个通常都是危险的信号。顺便说一下,我们使用xcode4.3来做这篇教程,4.2的版本也应该没有什么问题。
注意:为了跟随这篇教程,这个编译生成的app需要运行在ios5的模拟器上面。假如你运行这个app到你的设备上,你也会崩溃,但是他们可能不会发生和教程一样的情况。
在模拟器上面运行你的app,你将会看到发生了什么。
嘿,他崩溃了。
有两种最基本的crash类型常发生:SIGABRT(也叫EXC_CRASH)和EXC_BAD_ACCESS(也可能会是SIGBUS或者SIGSEGV)。
就crash而言,SIGABRT是一个比较好解决的,因为他是一个可掌控的crash。App会在一个目的地终止,因为系统意识到app做了一些他不能支持的事情。
EXC_BAD_ACCESS是一个比较难处理的crash了,当一个app进入一种毁坏的状态,通常是由于内存管理问题而引起的时,就会出现出现这样的crash。
幸运的是,第一种崩溃(也是大多数崩溃)是SIGABRT,SIGABRT通常会在xcode的Debug Output窗口(在窗口的右下角)输出一些错误的信息。假如你没有看到Debug Output窗口,在你的xcode窗口的右上角一组图标中点击中间那个,假如还是没有看到Debug Output窗口,你需要点击这个小窗口的右上角的中间那个图标,他靠近搜索框。在这个情况下,会展示一些下面东西:
- Problems[14465:f803] -[UINavigationController setList:]: unrecognized selector sent to
- instance 0x6a33840
- Problems[14465:f803] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
- reason: '-[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840'
- *** First throw call stack:
- (0x13ba052 0x154bd0a 0x13bbced 0x1320f00 0x1320ce2 0x29ef 0xf9d6 0x108a6 0x1f743
- 0x201f8 0x13aa9 0x12a4fa9 0x138e1c5 0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7
- 0x11a9b 0x2792 0x2705)
- terminate called throwing an exception
了解这些错误消息是非常重要的,因为他们包含了错误在那里的重要线索,一下就是需要关注的部分:
- [UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840
“unrecognized selector sent to instance XXX” 这条错误消息意味着你的app正在试着执行一个不存在的方法。这种情况的发生,主要是都是一个方法被错误的对象调用了(也就是这个对象没有这个方法,但是你调用了他,就错了)。例如在这里这个问题上,对象就是UINavigationController (在内存地址0x6a33840上),方法就是setList:。
知道crash的原因是很好的,但是你的第一行动目的就是指出这个错误的发生在代码的那个地方。你需要找到源文件的名字和这个错误方法在那一行。你通过使用call stack(就像堆栈跟踪(stacktrace)或者回溯(backtrace))就可以知道这些东西。
当你的程序crash了时,在xcode窗口的左边小窗口会启动Debug Navigator(调试导航)。他会展示在这个app中那个线程是活动的,并且高亮显示crash了的线程。通常他会是线程1,这个app的主线程,这个线程也是你会做最多工作的线程。假如你的代码里面使用了队列(queues)或者后台线程(background threads),这个app也可能会在其他的线程里面崩溃。
当前xocde就高亮显示了main.m里面的main()函数。但是那些东西并没有告诉你很多,所以你需要继续的向深层次的挖掘。
为了看到堆栈的更多信息,拖拽Debug Navigator底部的滑块到最右边。它将会展示出崩溃时全部的堆栈信息:
这个列表里面的每一项都是一个来这个app或者ios的framework里的方法或者函数。堆栈展示了当前活跃在这个app里面的方法或者方法。调试器(debugger)已经暂停了这个程序,并且所有的这些方法和函数在这个时候也被冻结了。
在底部的函数start(),第一个被调用。在他的执行里面的有些地方,main()函数在他之前。(Somewhere in its execution it called the function above it, main().)。他是应用程序的开始入口点,并且它经常在底部附近。Main()也叫UIApplicationMain()(这个针对的是ios哈,并不是其他所有程序都是这样的)。在这个编辑窗口里面用绿色箭头指示的那一行(就是在这个教程最开始前面程序崩溃时停止在那个图片上,高亮显示的部分)。
进一步来看看这个堆栈,UIApplication()在UIApplication对象里调用_run方法,_run方法里面又调用CFRunLoopRunInMode()方法,CFRunLoopRunInMode()方法里面又调用CFRunLoopSpecific()方法,就这样一直向下调用,一直到__pthread_kill。
所有在这个堆栈里面的函数和方法都是灰色的,除了main()函数。那是因为他们都来自内置的ios frameworks(ios内置框架)。所以没有针对他们可见的源码。
在这个堆栈里面唯一的东西就是你有main.m的源码,因此xcode的代码编辑器就显示了它,即使他不是这个崩溃的真正原因。但是这个经常混淆初学者,但是马上我将展示怎么样来弄懂它。
开个玩笑,点击这个堆栈里面的任意一项,你将会看到许多的汇编代码,这些你可能完全不理解:
加入我们得到那样的源码,我想很多人都会说:坑爹啊。
异常断点
你怎么样找到是代码里面的哪一行使app崩溃的?无论什么时候,你得到的一个想这样的堆栈路径,一个异常通过这个app抛出。(你多半会说因为堆栈里面有一个函数叫objc_exception_rethrow。)
当程序由于做了一些他不能完成的事情时,一个异常就会发生。你所看到的就是这个异常的结果:app做了一些错的事情,异常被抛出,xcode展示异常的结果。理想情况下,你想要的准确的看到异常在那里抛出的。
幸运的是,通过使用Exception Breakpoint(异常断点),你可以告诉xcode在一个特定的时候暂停这个程序。断点是一个在特定时刻暂停你的程序的调试工具。你将会第二篇教程里面看到更多关于他们的信息,但是现在你将会使用一个特殊的断点,它将会在抛出异常前暂停你的程序。
为了设置异常断点,我们不得不切换到Breakpoint Navigator(断点导航器):
在底部有一个小的加号(“+”)按钮。点击它,并且选择Add Exception Breakpoint:
一个新的断点将会被增加到这个列表里:
点击Done按钮使弹出的窗口消失。注意在xcode工具栏上面Breakpoints button(断点按钮)是有效的。加入你不想要带着任何断点运行你的app,你可以简单的开关这个按钮到off。但是现在,让它打开,并且再一次运行这个app。
太好了!代码编辑器现在停止并且指到了代码中的其中一行,不再在令人烦躁的汇编代码了,并且注意在在左边的的Debug Navigatot(调试导航器)里面显示的堆栈信息也不一样了。
显然的,问题就出在AppDelegate里面的application:didFinishLaunchingWithOptions:方法里:
- viewController.list = [NSArray arrayWithObjects:@"One", @"Two"];
仔细再次看看这个错误消息:
- [UINavigationController setList:]: unrecognized selector sent to instance 0x6d4ed20
在这个代码里面,“viewController.list = something”这种方式隐式的调用了setList:方法,也就是set方法,因为“list”是MainViewController类的一个属性。然而,通过这个错误消息,我们知道viewController这个变量没有指向MainViewController对象,而是指向了UINavigationController,所以显然的,UINavigationController没有“list”属性!所以这些变量在这里混淆了。
打开Storyboard文件,看看window的rootViewController属性实际上是指向那个的:
哈哈!Storyboard的最初的view controller是一个Navigation controller。这就是为什么window.rootViewController是一个UINavigationController对象,而不是你自认为的MainViewController。为了修改这里,使用下面的代码来替代application:didFinishLaunchingWithOptions:里面的:
- - (BOOL)application UIApplication *)application didFinishLaunchingWithOptions NSDictionary *)launchOptions
- {
- UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
- MainViewController *viewController = (MainViewController *)navController.topViewController;
- viewController.list = [NSArray arrayWithObjects:@"One", @"Two"];
- return YES;
- }
通过代码可以看出,首先你通过self.window.rootViewController得到UINavigationController,一旦你得到了上面的。你就可以通过请求navigation controller来得到topViewController,进而得到MainViewController。现在viewController变量就是指向了正确的对象了。
注意:一旦你得到“unrecognized selector sent to instance XXX”错误,你就需要检查这个对象是不是正确类型,并且检查它真的是有那个名字的方法么。你会经常发现你调用一个你认为是这个对象的方法,因为指针变量可能没有包含这个正确值,所以导致很多的错误。
另外一个经常出现错误的原因就是将方法名称拼写错误。一会儿你将会看到一个这样的例子。(译者:我个人认为有xcode的代码提示功能,这种错误应该还是比较少吧,多数应该出现在通过selector,或者传递函数指针的时候,应该会多点这个错误)。
你的第一个内存错误
你可能已经修复了你的第一个问题。再一次运行这个程序。坑爹啊,在同样的一行,又崩溃了,但是现在是EXC_BAD_ACCESS错误。那意味着这个app有内存管理的问题。
一个和内存相关的崩溃一般很难定位到源代码,因为这个恶魔可能很早就在程序中做了坏事了。假如一段有问题的代码混乱了内存结构,这样产生的蝴蝶效应可能会在之后很久才表现出来,并且总在不同的地方。
实际上,在你所有的测试中,这个bug可能永远不会出现,但是却在你的客户的设备上展露出它丑陋的脑袋。这种是很多人都不想的。
这种特别的崩溃但是却很容易修复。假如你看到你的代码编辑器,xcode其实一直就在警告你这一行代码。看到左边靠近行号的那个黄色三角形没有?那个指出一个编译警告。假如你点击那个黄色的三角形,xcode将会弹出一个“Fix-it”的建议,就像下面的一样:
这个代码使用了一系列的对象来初始化一个数组(NSArray),并且像那样的一系列的对象应该使用nil来终止,这个警告的标记就是想要表达一个这样的意思。但是代码却没有那样做,所以NSArray就很困惑,很迷茫。它试着读取一个不存在的对象,最后这个app艰难的崩溃了。
这种错误,你真的不应该犯,特别是xcode已经警告了你。修复这个错误,通过像下面一样增加一个nil(或者你可以简单的选择刚刚弹出来的菜单里面“Fix-it”):
- viewController.list = [NSArray arrayWithObjects:@"One", @"Two", nil];
“This class is not key value coding-compliant”
重新运行这个程序,看看为你准备的其他有趣的bug。信不信由你?它又在main.m里面崩溃了。虽然Exception Breakpoint任然起作用了,但是我们没有看见任何高亮的程序代码,这次的崩溃真的没有发生在任何程序代码里。这个调用堆栈证实了这点:这里面的方法没有一个属于的程序的,除了main():
假如你从上到下浏览一下这些方法的名字,有些问题发生在NSObject和Key-Value Coding。在那之下调用了[UIRuntimeOutletConnection connect]。我不知道那个是干什么的,但是看起来好像它做了连接outlet的一些事情。在那之下的一些方法是从nib中加载view。因此以上那些也给你一些线索。
但是,在xcode的调试窗口,并没有易懂的错误消息。那是因为没有异常被抛出。在xcode告诉你异常的原因之前,Exception Breakpoint已经暂停了这个程序。有些时候你会从Exception Breakpoint得到一些局部的错误消息,但是有些时候就得不到。
为了得到全部的错误消息,点击调试器工具栏上的“Continue Program Execution”按钮:
你可能需要点击好几次才可以,然后你将会得到错误消息:
- Problems[14961:f803] *** Terminating app due to uncaught exception 'NSUnknownKeyException',
- reason: '[ setValue:forUndefinedKey:]: this class is not
- key value coding-compliant for the key button.'
- *** First throw call stack:
- (0x13ba052 0x154bd0a 0x13b9f11 0x9b1032 0x922f7b 0x922eeb 0x93dd60 0x23091a 0x13bbe1a
- 0x1325821 0x22f46e 0xd6e2c 0xd73a9 0xd75cb 0xd6c1c 0xfd56d 0xe7d47 0xfe441 0xfe45d
- 0xfe4f9 0x3ed65 0x3edac 0xfbe6 0x108a6 0x1f743 0x201f8 0x13aa9 0x12a4fa9 0x138e1c5
- 0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7 0x11a9b 0x2872