跟我一起学习如何调试和修复可怕的应用程序崩溃问题吧!
要做的第一件事是:不要惊慌!
修复崩溃并不费劲。如果你已经阵脚大乱,可能是你把情况想的太严重了。不要指望说句咒语就能让bug奇迹般地消失,你需要采取有条不紊的方法,学会通过崩溃找出原因。
首先是要在你的代码中找出发生崩溃的确切位置:在哪个文件的哪一行。Xcode调试器会帮助你,不过你也要明白如何充分地运用它,这也正是本教程将向你展示的!
本教程面向所有开发人员,从初级到高级。即使你是一个经验丰富的iOS开发者,也可能会找到一些技巧和窍门。
入门
下载示例工程。正如你所看到的,这是一个错误的程序!当你在Xcode中打开该项目,显示至少有八个编译器警告,这通常是麻烦的前兆顺便说一下,本教程我们使用Xcode4.3,当然4.2版应该工作也很好。
注:要跟随本教程,应用程序需要运行在iOS 5的模拟器上。如果您运行应用程序在真机上,也还是会崩溃,但崩溃发生的顺序可能不同。
运行这个app,看一看发生了什么。
嗨,它崩溃了!:-(
崩溃的发生分两种情况:SIGABRT(也称为EXC_CRASH),EXC_BAD_ACCESS(也可以显示在SIGBUS或SIGSEGV的名字下)。
SIGABRT是很好解决的,因为它是可控的。终止该app的原因是系统发现这个app做了一些未经允许的事。相反,EXC_BAD_ACCESS难解决很多,因为它只发生在应用损坏的状态下,通常是由内存管理问题引起的。
幸运的是,这第一次崩溃(后续还多着呢)是一个SIGABRT。SIGABRT崩溃发生时通常都会在Xcode的调试输出窗格(窗口的右下角)输出一个错误消息。(如果你看不见调试输出窗格,点击Xcode窗口右上角那三个视图图标的中间那个,就会显示出调试区域了。如果调试输出窗格还是看不到,你可能还得点击调试区域右上角那三个视图图标(挨着搜索区域的那三个)的中间那个)。在本例中,崩溃信息是这样的:
2013-08-15 09:54:39.538 Problems[393:c07] -[UINavigationController setList:]: unrecognized selector sent to instance 0x6ed5800
2013-08-15 09:54:39.563 Problems[393:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UINavigationController setList:]: unrecognized selector sent to instance 0x6ed5800'
*** First throw call stack:
(0x14a4052 0xea4d0a 0x14a5ced 0x140af00 0x140ace2 0x251f 0x129d6 0x138a6 0x22743 0x231f8 0x16aa9 0x138efa9 0x14781c5 0x13dd022 0x13db90a 0x13dadb4 0x13daccb 0x132a7 0x14a9b 0x23c2 0x22f5)
terminate called throwing an exception(lldb)
学会解读这些错误消息很重要,因为它们含有能指明错误如何发生的重要线索。这里,有用的部分是下面这句:-[UINavigationController setList:]: unrecognized selector sent to instance 0x6ed5800这条错误消息的意思是该app试图调用了不存在的方法。通常这种崩溃都是由于对象调用了它自己没有实现的方法。这里的对象是UINavigationController(内存地址 0x6ed5800),方法是 setList:。
知道崩溃的原因是一个良好的开端,但你最先要做的是修复代码中的错误。你需要找到源文件的名称和错误发生的行数。你可以借助调用栈来寻找(也被称为堆栈跟踪或回溯)。
app崩溃时,Xcode窗口的左窗格会切换到Debug导航。它显示了应用程序中的活动线程,并突出崩溃的线程。通常线程1,就是app的主线程,也正是做绝大部分工作的地方。如果你的代码使用了队列或后台线程,那么app也可以在其他线程崩溃。
目前,Xcode把main.m文件的main()函数作为问题的根源突出显示了出来。这并不能说明多少,所以你必须深入挖掘信息。
要看到更多的调用堆栈,拖动调试导航器底部的滑块一直到右边。这会显示崩溃时完整的调用堆栈信息:
列表中的每一条都是app或者ios框架中的一个函数或方法。堆栈调用会展示出当前在app中活动的函数或方法。调试器已经终止了app,及时冻结了所有函数和方法。
最底部的函数是最先调用的。当它执行到某处时调用了它上面的函数。这是应用程序的起点,它总是会在底部。 main()依次调用 UIApplicationMain()。就是编辑窗口绿色箭头指向的那行(xcode右侧窗口中突出显示那行)。
进一步追溯堆栈,UIApplicationMain()调用了UIApplication对象的方法_run,它有调用了CFRunLoopRunInMode(),接着又调用了CFRunLoopRunSpecific(),依次类推,直到调用__pthread_kill。
除了main(),堆栈调用中的其他所以函数和方法都是灰色的。这是因为他们来自内置的iOS框架。看不到它们的源代码。
在这个堆栈追踪中,可以看到源代码的只有main.m,正如xcode源码编辑器显示的。不过这并不是崩溃产生的真正源头。这往往会弄晕了新的开发者,不过一会儿我会告诉你如何理解它。
幸好,点击堆栈追踪中的任何一个条目,都会看到一堆汇编代码,而这些很可能对你一点也没有。
噢,但愿我们能有源代码!:-)
异常断点
那么,如何在代码里找到引起崩溃的那一行呢?嗯,无论何时当你看到象这样的堆栈追踪时,程序都会抛出一个异常。
当程序被发现正在做一些它不该做的事时,就会发生异常。 你现在看到的是这个异常的后果:应用程序做错了什么,异常被抛出,,Xcode显示给你结果。 理想情况下,你想要清楚地看到该异常被抛出的地点。
所幸,你可以使用异常断点让xcode在那一瞬间暂停程序。断点是一种调试工具,它可以让你的app在某一时刻暂停。在本教程的第二部分,你会看到更多的断点,不过此刻,你看到的这个断点会在异常抛出前一刻暂停程序的。
要设置异常断点,我们必须切换到断点导航:
底部是一个小的+按钮。单击此按钮并选择“Exception Breakpoint”断点:
一个新的断点将被添加到列表:
单击“完成”按钮关闭弹窗。注意,xcode工具栏中的断点按钮现在已经被开启。如果你要运行的程序没有任何断点,你就可以点击这个按钮来关闭它。但现在,保留它,并再次运行应用程序。
真好!源代码编辑器现在指向了代码中的一行——没有讨厌的汇编东东——注意左边的调用堆栈(你可能需要通过调试导航切换到调用堆栈,这取决于你在xcode中的设置)看起来也不同了。
显然,罪魁祸首就是AppDelegate文件中application:didFinishLaunchingWithOptions: 方法里的下面这一行:
viewController.list = [NSArrayarrayWithObjects:@"One",@"Two"];
再次来看错误信息:
2013-08-15 14:42:21.684 Problems[835:c07] -[UINavigationController setList:]: unrecognized selector sent to instance 0x6cb71c0
代码中,viewController.list=xxx实际上是在调用setList:方法,因为list是MainViewController 类里的一个property。然而,根据错误消息显示,viewController变量不指向MainViewController对象,而是指向一个UINavigationController 对象——当然,UINavigationController没有 “list” property!这里把它们混淆了。打开Storyboard文件,看一看窗口的rootViewController实际上指向什么:
- (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的一个引用,一旦有了这个引用,你就能通过询问导航控制器的topViewController取得指向MainViewController的指针。现在,viewController变量应该指向了正确的对象。
注意:无论何时得到“unrecognized selector sent to instance XXX” 的错误,请检查对象的类型是否正确以及它是否有这个名称的方法。通常你会发现你在对一个不对的对象调用这个方法,由于指针变量所指向的值不对,它根本就不是你要的那个对象。
这种错误的另一常见原因是 方法名称的拼写错误。稍后你会看到一个这样的例子。
你的第一个内存错误
此刻应该已经修复了第一个问题。再次运行该应用程序。哎呀,它在同一行又崩溃了,只是这次是一个EXC_BAD_ACCESS错误。这意味着应用程序有一个内存管理问题。
与内存相关的崩溃源头是很难定位到的,因为罪魁祸首很可能是程序之前已经执行过的。如果发生故障的代码片段破坏了内存结构,直到很久以后此结果才会出现在一个完全不同的地方。
事实上,测试时这个bug可能永远都不会出现,只会在你的用户们的设备上张牙舞爪。你不希望这样的事情发生!
然而,这种特殊的崩溃是很容易修复的。如果你看看源代码编辑器,Xcode已经在这一行警告过你了。看到紧挨着行标左侧的黄色三角了吗?这是编译器警告。如果你点击黄色三角形,Xcode应该会弹出一个这样的“修复”建议:
在警告中提及的地点,代码通过赋值一个对象列表初始化了一个NSArray 对象,而此种列表应该是以nil结尾的。不过代码并没有这么写,所以NSArray被弄迷糊l
了。它试图读取不存在的对象,所以app崩溃了。
这是一个不该犯的错误,特别是xcode都已经警告过你了。通过下面的方法为列表添加nil来修复代码(或者,你可以简单地选择“修复”菜单选项):
viewController.list = [NSArray arrayWithObjects:@"One",@"Two",nil];
“这个类不支持键值编码兼容”
再次运行应用程序,看看这个项目里还为你藏了哪些其他有趣的bug。你是怎么知道的?它再次在main.m里崩溃。异常断点仍处于启用状态,我们看不到应用程序的任何源代码处于高亮显示,这次崩溃真的没有发生在app的源代码中。调用堆栈证实了这一点:除main():以外,其余方法都不是app的。
如果从上往下浏览这些方法名,你会发现好多都是关于NSObject的和键 -值编码的。其下方有一个 [UIRuntimeOutletConnection connect]的调用。我不知道那是什么,不过看起来是和连接outlets有关的。其下方是一些关于从nib文件加载views的方法。以上这些就已经给我们提供了线索了。
不过Xcode的调试窗格中并没有直观的错误信息。这是因为还没有异常被抛出。在调试窗格告知我们异常发生原因之前,异常断点已经暂停了程序。在启用异常断点的情况下,有时可以获得部分错误信息,有时则不能。
要看到完整的错误信息,点击“调试”工具栏上的“继续执行程序”按钮:
你可能还需要再次点击它,然后会获得完整的错误信息:
Problems[14961:f803] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<MainViewController 0x6b3f590> 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 0x27e5) terminate called throwing an exception
像之前一样,你可以忽略底部的那些数字。它们代表了调用栈,但在左侧调试导航栏里,你已经拥有了一个更方便 - 可读的格式。
有意义的点是:
- NSUnknownKeyException
- MainViewController
- “this class is not key value coding-compliant for the key button”
正如我们已经建立的,这一切都发生在加载nib文件时。虽然这个app使用的是storyboard,不是nibs,但就内部实现而言,storyboard就是nibs的一个集合,故一定是storyboard那里出了问题。
检查MainViewController中的outlets:
在连接检查器里,可以看到试图管理器中心的按钮是与MainViewController的按钮出口连接着的。storyboard/nib连接了出口button,但是错误信息指出不能找到此出口。
看看MainViewController.h:
button的@property 定义是存在的,那么问题在哪呢?如果你有注意到编译警告的话,可能已经找到了。
要是还没有,就去查看一下MainViewController.m里的@synthesize list.现在看到问题所在了吗?
代码实际上并没有@synthesize button。
未完待续!