from:http://article.ityran.com/archives/1143
有这样一种情形:当我们正在快乐的致力于我们的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 0x6a33840Problems[14465:f803]***Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840'***Firstthrow call stack:(0x13ba0520x154bd0a0x13bbced0x1320f000x1320ce20x29ef0xf9d60x108a60x1f7430x201f80x13aa90x12a4fa90x138e1c50x12f30220x12f190a0x12f0db40x12f0ccb0x102a70x11a9b0x27920x2705) 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.'***Firstthrow call stack:(0x13ba0520x154bd0a0x13b9f110x9b10320x922f7b0x922eeb0x93dd600x23091a0x13bbe1a0x13258210x22f46e0xd6e2c0xd73a90xd75cb0xd6c1c0xfd56d0xe7d470xfe4410xfe45d0xfe4f90x3ed650x3edac0xfbe60x108a60x1f7430x201f80x13aa90x12a4fa90x138e1c50x12f30220x12f190a0x12f0db40x12f0ccb0x102a70x11a9b0x28720x27e5) terminate called throwing an exception
就像之前的一样,你可以忽略下面的那些数字。他们展示了调用堆栈,但是在调试导航器的左边有更加直观的堆栈调用展示。
有趣的部分是:
NSUnknowKeyException
MainViewController
“this class is not key value coding-compliant for the key button”
这个异常的名字为NSUnknownKeyException,它是这个错误很好的指示器。它告诉你在某个地方有一个“unknown key”。这个某一个地方通常就是MainViewController,并且这个key就是“button”。
既然我们已经确定了,所有这些都是发生在装载nib的时候。这个应用使用的是storyboard,而不是nib文件,但是其实storyboard内部就是nib的集合(也就是可以有很多的nib),因此这个错误就在这个storyboard中。
检查一下MainViewController的outlets:
在Connections Inspector(连接检测器)里,你可以看见在viewcontroller中间的UIButton是连接到MainViewController的 “button”outlet上的。因此storyboard引用了一个名叫“button”的outlet,但是通过这个错误消息说明它找不到这个 outlet。
让我们来看看MainViewController.h:
@interfaceMainViewController:UIViewController@property(nonatomic, retain)NSArray*list;@property(nonatomic, retain)IBOutletUIButton*button;-(IBAction)buttonTapped:(id)sender;@end
这里是为这个“button”定义了外部连接属性的(@property),因此这个问题是什么呢?假如你仔细观察了编译警告的话,你可以已经知道是什么地方的问题了。
假如还不知道的话,检查一下MainViewController.m的@synthesize的内容的话。你现在看出问题没有啊?
这个代码其实没有@synthesize这个button的属性。它(@synthesize)其实是告诉MainVIewController他自己有 个“button”的属性,提供一个后台实例变量,并且提供getter和setter方法(这就是@synthesize所做的)。
把下面的增加到MainViewController.m里面已经存在的@synthesize行的下面来修复这个问题:
@synthesize button = _button;
现在这个app应该不会在你运行的时候崩溃了!
注意:“this class is not key value coding-compliant for the key XXX”的错误经常都是由于你装载这个nib,但是里面引用的一些熟悉可能不存在。特别是当你在代码中移除了outlet属性后,但是你却没有在nib中 移除这个连接。
Push the Button
现在这个app正常工作,或者至少说启动的时候没有问题。是时候来点击这个按钮了。
哇!这个app崩溃在main.m里面,并且伴随着SIGABRT。在调试窗口打印出的错误消息是:
Problems[6579:f803]-[MainViewController buttonTapped]: unrecognized selector sent to instance 0x6e44850
堆栈跟踪也不是很有启发。只是列出了一些和一个方法相关的或者发送了事件并且执行了动作的方法,但是你已经知道到了被涉及的动作了。毕竟,你点击了一个按钮,这个按钮的IBAction方法应该被调用。
当然你之前应该已经看过了这个错误消息。一个被调用的方法不存在。这个时候目标对象应该是MainViewController,由于动作方法经常存在于 一个包含按钮的view controller里面,所以这个看起来是正确。并且你看MainViewController.h文件,这个IBAction方法确实在里面:
-(IBAction)buttonTapped:(id)sender;
是这样的吗?错误消息显示这个方法的名字是buttonTapped,但是MainViewController的方法却是buttonTapped: (注意冒号),由于这个方法需要接受一个参数(名字是sender),所以在方法名字后面有个冒号。从这个错误消息看出,这个方法没有冒号,因此不需要参 数。所以这个方法看其实应该是这样的:
-(IBAction)buttonTapped;
这个里发生了什么?这个方法最初的时候不需要参数(有些时候这样动作方法是被允许的),并且在那个时候,他为这个按钮在storyboard里面连接了 Touch Up Inside的时间方法。然而,在那之后某个时候,这个方法的形式被修改为包含了一个“sender”参数,但是,却没有去更新storyboard。
你可以在storyboard里面看看,在这个按钮的连接检测器:
第一,断开Touch Up Inside 事件(点击这个小“X”),然后再次连接它到MainViewController里,但是这次选择这个buttonTapped:方法。注意在连接检查器里面看看这个方法后面是有一个冒号的。
运行这个app,再一次点击按钮。我们又得到了这个“unrecognized selector”消息,但是这次他正确的定位到了buttonTapped:方法里面。
Problems[6675:f803]-[MainViewController buttonTapped:]: unrecognized selector sent to instance 0x6b6c7f0
假如你仔细看的话,编译器警告应该又给你指出解决方案。Xcode提出MainViewController的实现是不完整的。特别的,buttonTapped:方法没有被发现。
是时候看看MainViewController.m了,在这里确实是有buttonTapped:方法啊……………..等等,拼写错误了:
-(void)butonTapped:(id)sender
很简单的修改,重命名这个方法:
-(void)buttonTapped:(id)sender
提示:你没必要声明这个方法为IBAction,假如你觉得这样是很优雅的,你可以这样做。
注意:假如你仔细注意到这些编译器警告的话,这些问题很容易看出来的。就个人而言,我把所有的警告当成严重的错误(在xcode里面的编译设置 (Build Settings)里面可以设置警告作为错误提示的),在运行程序以前,我会修改所有的。Xcode在指出愚蠢的错误表现的相当好,就像这里这样,并且注 意到这些提示是很明智的。
Messing with Memory(混乱内存)
经过了这么多,你知道崩溃一直在继续从未停止过。运行这个app,点击按钮,然后等待崩溃。好,现在就来了:
这里是另一种EXC_BAD_ACCESS崩溃。幸运的是,xcode已经准确给你指示出位置在那里了,在这个buttonTapped:方法里面:
NSLog("You tapped on: %s", sender);
有些时候,你可能在上面花费一些时间才会反应过来,但是xcode提供了帮助,仅仅需要点击这个黄色的三角形来看这个错误是什么:
NSLog呈现一个Objective-c类型的字符串,而不是一个c字符串,因此插入一个@符号来修改它:
NSLog(@"You tapped on: %s", sender);
你将会注意到这个警告的黄色三角形依然没有消失。这是因为在这行还有另外一个bug,这个bug可能会或者可能不会使你的程序崩溃。有些时候这个代码工作很好,或者现在看起来很好,但是有些时候他就会崩溃。(特别是有些时候只在你的客户的设备上面,绝不会在你的设备上)。
让我们来看看这个新的警告:
这个“%s”专门为c语言类型的字符串。一个c类型的字符串就是把这个内存分成片段(一个老式的字节数组),通过一个所谓的”NUL character”(其实就是一个为0的值)来终止。例如这个“Crash!”看起来就是这样的:
无论是什么时候,你使用一个函数或者方法来操作这个c类型的字符串,你不得不确定这个字符串是以一个0值来结尾的,否则这个函数将不知道这个字符串已经结束了。
现在来看看,当你指定了在NSlog()中用“%s”来格式化字符串,或者在NSString 的stringWithFormat里面,这个变量将会被当做是一个c类型的字符串。假如这个“sender”指向一个包含0字节的内存,这个 NSlog()将不会崩溃,但是输出的东西就会像这样:
You tapped on: xËj
再一次运行这个app,点击这个按钮,等待它崩溃。现在在Debug窗口的左边部分,右击“sender”,并且选择“view Memory of “*sender””选项(确保是选择的是带有星号的sender)。
Xcode将会展示出这个内存地址的内容,恰恰这个就是NSlog()打印出来的内容。
然而,这里并不能保证这里有空位(结束标志位),所以你完全很容易执行到一个EXC_BAD_ACCESS的错误。 假如你经常在模拟器上面测试的话,这个很长时间都可能不会发生,然而这种情况一般都是在很特殊的情况环境下就可能发生。所以这种类型的bug很难跟踪。
当然,在这种情况下,xcode已经警告你这个错误的格式化字符串,因此这个特别的bug是很容易发现的。但是无论什么时候,你使用c类型的字符串或者手动直接操作内存的,都应该非常的小心的不要混乱了其他的内存。
假如你非常的幸运,这个app将会经常崩溃,这个bug很容易找到,但是通常情况是这个app会崩溃在某个时候,而且这个问题很难重现!之后寻找这个bug将会是一个史诗般的工程,十分麻烦。
修复这个NSLog()的形式,就像下面的一样:
NSLog(@"You tapped on: %@", sender);
运行这个app,并且再一次点击这个按钮。现在这个NSLog()做了他能做的了,并且看起来好像不会崩溃在buttonTapped:这个函数里面了。
和调试器交朋友(Making Friends With the Debugger)
看看这最近的崩溃,xcode指示到了这一行:
[self performSegueWithIdentifier:@"ModalSegue" sender:sender];
在Debug窗口里面没有消息打印出来。你可以点击这个继续执行这个程序的按钮,就像前面一样,但是你也可以在调试器里面输入一个命令来得到这个错误消息。这样做的好处就是,这个app可以保持暂停在这个同样的地方。
假如你准备在模拟器里面运行这个,你可以在“(lldb)”提示的后面输入下面的:
(lldb) po $eax
LLDB在xcode4.3或者之后的版本里面是默认的调试器。假如你正在使用老一点版本的xcode的话,你又GDB调试器。他们有一些基本的相同的命 令,因此假如你的xcode使用的是“(gdb)”提示,而不是“(lldb)”提示的话,你也能够更随一起做,而没有问题。
“po”命令是“print object”(打印对象)的简写。“$eax”是cup的一个寄存器。在一个异常的情况下,这个寄存器将会包含一个异常对象的指针。注意:$eax只会在模拟器里面工作,假如你在设备上调试,你将需要使用”$r0″寄存器。
例如,假如你输入:
(lldb) po [$eax class]
你将会看像这样的东西:
(id) $2 =0x01446e84NSException
这些数字不重要,但是很明显的是你正在处理的NSException对象在这里。
你可以对这个对象调用任何方法。例如:
(lldb) po [$eax name]
这个将会输出这个异常的名字,在这里是NSInvalidArgumentException,并且:
(lldb) po [$eax reason]
这个将会输出错误消息:
(unsignedint) $4 =114784400Receiver() has no segue with identifier 'ModalSegue'
注意:当你仅仅使用了“po $eax”,这个命令将会对这个对象调用“description”方法和打印出来,在这个情况下,你也会得到错误的消息。
因此解释下那是什么情况:你正在尝试执行一个名叫“ModalSegue”的segue,但是很显然,在MainViewController里面并没有这样的的segue。
Storyboard展示出来这个segue是存在的,但是你忘记了设置它的标示,一个典型的错误:
改变这个segue的标示为“ModalSegue”。再一次运行这个app,等待一下,点击这个按钮 ,这个时候不再有crash了!但是这里只是我们下个部分的开端——-tableview不应该是空的!
在这个教程的第一部分,我们介绍了SIGABRT和EXC_BAD_ACCESS错误,并且举例说明了一些使用xcode调试器(Xcode debugger)和异常断点(Exception Breakpoints)解决问题的策略。
但是我们的app仍然有一些问题!就像我们看到的,他工作的并不是很好,并且这里仍然有许多潜在的可能崩溃的问题。
幸运的是,在这个教程的第二部分,也是最后一部分,我们可以学习更多的技术来处理这些问题。
所以我们就不在啰嗦了,让我们回到继续修正这个充满bug的app中吧!
Getting Started: When What’s Supposed to Happen, Doesn’t
在第一部分我们停止的地方,经过许多的调试工作之后,我们运行这个程序他是不会崩溃的。但是他却展现了一个没有预料到的空的table,就像下面一样:
当你觉得一些事情应该发生,但是却没有发生的时候,这里有些你可以使用一些技巧来排除问题。在这个教程里面,我们首先是学习使用NSlog来解决这个问题。
这个table view controller的类是ListViewController。在一系列的任务执行之后,这个app应该装载ListViewController, 并且在屏幕上面显示出来。你可以做一个测试,来确定view controller的方法是执行了的。所以viewDidLoad这个方法看起来应该是一个好地方来做测试。
在ListViewController.m,增加一个NSLog()到viewDidload,就像下面一样:
-(void)viewDidLoad {[super viewDidLoad];NSLog(@"viewDidLoad is called");}
当你运行这个app时,你应该期望当我们点击了“Tap Me”按钮后在调试窗口看到“viewDidLoad is called”这样文字。现在就来试试,点都不惊讶,在调试窗口什么也没有出现。那就意味着ListViewController类根本没有被使用!
这个多半意味着,你可能忘记了告诉storyboard你想要为table view controller场景使用ListViewController类。
由上图我们可以看出,在身份检查器(Identity Inspector)的类属性区域是设置的默认值UITableViewController。改变这个Custom Class下面的class为ListViewController,然后再一次运行这个app。现在在调试窗口应该就会出现“viewDidLoad is called”文字:
PProblems[18375:f803]You tapped on:<UIRoundedRectButton:0x6894800; frame =(119189;8237); opaque = NO; autoresize = RM+BM; layer =<CALayer:0x68948f0>>Problems[18375:f803] viewDidLoad is called
但是这次app将会再一次崩溃,但是却是一个新的问题。
注意:一旦你的代码好像没起什么什么作用的话,放置一些NSLog()在确切的地方,来看看是否这个方法是被执行了的和cpu通过怎么样路径执行这个方法。使用NSLog()来测试你假设将会执行的代码。
Assertion Failures
这个新的有趣的崩溃。它是一个SIGABRT,并且在调试窗口打印出来的是以下消息:
Problems[18375:f803]***Assertion failure in-[UITableView _createPreparedCellForGlobalRow: withIndexPath:],/SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:6072
我们得到的是一个执行UITableView的一些方法的一个“断言错误(assertion failure)”。当某些东西出错了之后,一个断言是一个内部相容性的检查器,并且会抛出一个异常。你也可以放置断言在你的代码里。例如:
-(void)doSomethingWithAString:(NSString*)theString {NSAssert(string!=nil,@"String cannot be nil");NSAssert([string length]>=3,@"String is too short");...}
在上面的方法里面,我们让一个NSString对象作为这个函数的变量,但是代码却不允许调用者传递一个nil或者长度小于3的字符串。假如这些条件中的一个不匹配的话,这个app将会终止,并且抛出一个异常。
你可以使用断言来作为一个防御性编程技术,因此你应该确定这个就是我们想要的代码行为。断言通常只在调试编译下有用的,因此他们对发布到app store的最终的app是没有运行时的影响的。
在这个情况下,某些情况触发了一个UITableView的断言错误,但是你并没有完全确定在那个地方。App也是停止在main.m里面,并且在执行堆栈里面只包含了框架(framework)的方法。
从这些方法的名字,我们可以猜测这个错误发生在重画这个tableview的某些地方。例如,我们可以看到layoutSubviews和_updateVisibleCellsNow:这些名字的方法。
继续运行这个app来看看是否可以得到一些比较好的错误消息—–记住,现在只是在抛出异常的时候暂停了程序,并没有崩溃。点击继续程序按钮,或者在调试窗口键入下面的命令:
(lldb) c
你可能不得不多点击几次继续按钮,“c”命令也是一个简短的继续指令,和点击继续按钮一个效果,并不是就直接执行到最后。
现在这个调试窗口喷发出一些比较有用的信息:
***Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'***Firstthrow call stack:(0x13ba0520x154bd0a0x1362a780x99a2db0xaaee30xab5890x96dfd0xa58510x503010x13bbe720x1d6492d0x1d6e8270x1cf4fa70x1cf6ea60x1cf65800x138e9ce0x13256700x12f14f60x12f0db40x12f0ccb0x12a38790x12a393e0x11a9b0x27220x2695) terminate called throwing an exception
太好了,这是一个相当好的一个线索。显然这个UITableView的数据源没有从tableView:cellForRowAtIndexPath:方法返回一个有效的cell,因此在ListViewController.m方法里面增加一些调试输出信息来看看:
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {staticNSString*CellIdentifier=@"Cell";UITableViewCell*cell =[tableView dequeueReusableCellWithIdentifier:CellIdentifier];NSLog(@"the cell is %@", cell); cell.textLabel.text =[list objectAtIndex:indexPath.row];return cell;}
你增加一个NSLog()标记。再一次运行这个app,看看输出了什么:
Problems[18420:f803] the cell is(null)
从以上信息我们可以看出,调用dequeueReusableCellwithIdentifier:返回的却是nil,这就意味着使用“Cell”作为标识符的cell可能不存在(因为这个app使用的是标准的cell的storyboard)。
当然,这也是愚蠢的bug,并且毫无疑问的是,在以前解决这个需要很长的时间,但是现在却不是了,因为xcode已经通过静态编译警告了 你:“Prototype cells must have reuse identities。(标准的cell必须有重用的标识)”。这个是不能忽视的警告:
打开storyboard,选择这个标准的cell(在tableview的顶端,并且显示的是“Title”的单独的一个cell),并且设置cell的标识符为“Cell”:
将那个修复了之后,所以的编译警告应该没有了。运行这个app,现在这个调试窗口应该会打印出来:
Problems[7880:f803] the cell is<UITableViewCell:0x6a6d120; frame =(00;32044); text ='Title'; layer =<CALayer:0x6a6d240>>Problems[7880:f803] the cell is<UITableViewCell:0x6877620; frame =(00;32044); text ='Title'; layer =<CALayer:0x6867140>>Problems[7880:f803] the cell is<UITableViewCell:0x6da1e80; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9fae0>>Problems[7880:f803] the cell is<UITableViewCell:0x6878c40; frame =(00;32044); text ='Title'; layer =<CALayer:0x6878f60>>Problems[7880:f803] the cell is<UITableViewCell:0x6da10c0; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9f240>>Problems[7880:f803] the cell is<UITableViewCell:0x6879640; frame =(00;32044); text ='Title'; layer =<CALayer:0x6878380>>
Verify Your Assumptions
你的NSLog()打印出来的消息,已经告诉我们6个table view cell被创建了,但是在table上面什么都看不见。怎么回事呢?假如你在模拟器里面到处点击一下,你将会注意到tableview中6个cell中的 第一个却能够被选中。所以,显然cells都是存在的,只是他们都是空的:
是时候需要更多的调试记录了。将先前的NSLog()标记改变一下:
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {staticNSString*CellIdentifier=@"Cell";UITableViewCell*cell =[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; cell.textLabel.text =[list objectAtIndex:indexPath.row];NSLog(@"the text is %@",[list objectAtIndex:indexPath.row]);return cell;}
现在你打印出来就是你的数据模块的内容。运行这个app,看看显示出来的是什么:
Problems[7914:f803] the text is(null)Problems[7914:f803] the text is(null)Problems[7914:f803] the text is(null)Problems[7914:f803] the text is(null)Problems[7914:f803] the text is(null)Problems[7914:f803] the text is(null)
上面的很好的解释了为什么在cell里面什么都没有看到的原因:因为这个文字(text)始终是nil。然而,假如你检查你的代码,并且在initWithStyle:方法里面显示的添加了很多的字符串到list array里面:
[list addObject:@"One"];[list addObject:@"Two"];[list addObject:@"Three"];[list addObject:@"Four"];[list addObject:@"Five"];
就像上面那样,这是测试你的假设是不是正确的一个很好的方法。可能你还想更准确的看看这个array里面到底有什么东西。改变先前在tableView:cellForRowAtIndexPath:里面的NSLog()为这样:
NSLog(@"array contents: %@", list);
至少这样可以给你展示一些东西。运行这个app。假如你还没准备好猜测会发生什么情况,调试窗口已经给你打印出来了:
Problems[7942:f803] array contents:(null)Problems[7942:f803] array contents:(null)Problems[7942:f803] array contents:(null)Problems[7942:f803] array contents:(null)Problems[7942:f803] array contents:(null)Problems[7942:f803] array contents:(null)
哈哈,你的脸色瞬间阴沉下来。上面的代码居然没有起作用,因为你可能忘了在首先为这个array对象申请内存空间。这个“list”所以一直为nil,因此调用addObject: 和objectAtIndex:不会起任何的作用。
你应该在你的view controller被装载的时候为这个list对象分配空间,因此在initWithStyle:方法里面应该是一个不错的选择。修改那个方法为:
-(id)initWithStyle:(UITableViewStyle)style {if(self==[super initWithStyle:style]){ list =[NSMutableArray arrayWithCapacity:10];[list addObject:@"One"];[list addObject:@"Two"];[list addObject:@"Three"];[list addObject:@"Four"];[list addObject:@"Five"];}returnself;}
试一试。我晕,依然什么都没有!调试窗口输出依然是:
Problems[7971:f803] array contents:(null)...and so on ...
经过了这么多假设和修改,但是还是什么都没有,这些真的是非常令人沮丧啊,但是请记住你可能会一直继续到最后,直到你弄清楚了所有的假设。所以现在的问题就是难道initWithStyle:没有被调用?
Working With Breakpoints
你可能又会在代码里面放置另外一个NSLog()标志,但是其实你完全可以使用另外的工具:断点( breakpoints)。你已经看到过无论什么时候只要有异常抛出的时候,程序就会终止的异常断点(Exception Breakpoint)了。你其实也可以增加其他的断点,并且可以放置到代码的任何地方。一旦你的程序运行到断点的地方,这个断点就会被触发,并且程序就 会进入调试模式。
你可以通过点击代码编辑区前面的行号来放置特殊的断点:
这个蓝色的箭头所指示的那一行就有一个断点了。你也可以在断点导航器(Breakpoint Navigator)里面看到这个新的断点:
再一次运行这个app。假如initWithStyle:确实是会被调用的话,那么你点击了“Tap Me!”按钮之后,当这个ListViewController被装载的时候,这个app将会暂停,并且会进入调试器。
可能正如你所料的,什么事情也没有发生。initWithStyle:没有被调用。其实这个是可以讲得通的,因为view controller是从storyboard(或者xib)中装载的,所以使用的应该是initWithCoder:方法。
将之前initWithStyle:方法替换为initWithCoder::
-(id)initWithCoder:(NSCoder*)aDecoder {if(self==[super initWithCoder:aDecoder]){ list =[NSMutableArray arrayWithCapacity:10];[list addObject:@"One"];[list addObject:@"Two"];[list addObject:@"Three"];[list addObject:@"Four"];[list addObject:@"Five"];}returnself;}
并且保持断点在这个方法上面,来看看它是怎么工作的:
一旦你点击了那个按钮,这个app将会进入调试器:
以上的情况并不是意味着这个app崩溃了!它只是在这个断点处暂停了。在左边的执行堆栈里面(假如你没有看到执行堆栈的话,你可能需要切换到调试导航 器),你可以看到你是从buttonTapped:到这里的。这个调试导航器里面,我们看到执行了一系列的UIKit的方法,并且装载了一个新的view controller。(顺便说句,断点是一个非常好的工具来指出这个系统是怎么工作的。)
如果想要离开你之前停留的地方,继续运行这个程序,简单的就是点击继续程序运行按钮,或者在调试控制台中输入“c”。
显然的是,一切并没有如我们料想的一样,这个app又奔溃了。我告诉过你,它有很多bug的。
注意:在你继续之前,在initWithCoder:移除断点或者使断点无效。因为他已经展现了他的目的,所以现在它可以离开了。
你可以在显示行号的的地方右击断点,并且在弹出的菜单中选择删除断点。你也可以拖出这个断点离开窗口,或者在断点调试器里面移除。
假如你并不想移除这个断点,你可以简单的使断点无效。为了达到这个目的,你可以使用右击弹出菜单,或者左击一次这个断点。判断这个断点是否有效,你可以看看这个断点的颜色,当为浅蓝色了就是无效了,深蓝色就是有效的。
Zombies!
回到这个崩溃。它是一个EXC_BAD_ACCESS,幸运的是调试器指到了他发生在那里,在tableView:cellForRowAtIndexPath:
这是一个EXC_BAD_ACCESS崩溃,意味着在你的内存管理里面有bug。不像SIGABRT,你将不会得到很明朗的错误消息。然而你可以使用一个让你看到曙光的调试工具:Zombies!
打开这个项目的scheme editor:
选择Run 选项,然后选择Diagnosics标签。勾上Enable Zombie Objects选项:
现在运行这个app。这个app仍然崩溃,但是现在你将会得到下面的错误消息:
Problems[18702:f803]***-[__NSArrayM objectAtIndex:]: message sent to deallocated instance 0x6d84980
上面这个就是zombie enable 工具所做的,做个小概括:无论什么时候你创建了一个新对象(通过发送“alloc”消息),一块内存将会为这个对象的实例变量保留。当这个对象被释放,他的保留计数(retain count)变成0,这块内存将会被释放。
但是,你可能仍然有许多的指针指向这个已经失效的内存,这些都是建立在假设这里有一个有效的对象存在的情况下。假如你程序的某些部分试着使用这个野指针,这个app将会伴随着EXC_BAD_ACCESS的错误崩溃掉。
(假如你是很幸运的话,这个程序将会崩溃。假如你没那么幸运的哈,这个app将会使用这个死亡的对象,各种各样的破坏可能相继发生,特别是某个指针所指向的这个内存区域已经被一个新的对象重新分配了。)
当这个zombie工具被启用之后,即使这个对象被释放了,这个对象的内存也不会被清理。所以,那块内存将会被标记为“长生不死的”。假如你试着之后又去 使用这块内存,这个app能够意识到你的错误操作,并且app将会抛出“message sent to daellocated instance”错误并且终止运行。
因此这就是之前发生的事。这行就是使用了不死的对象:
cell.textLabel.text =[list objectAtIndex:indexPath.row];
这个cell对象和他的textLabel应该是好的,那么indexPath也应该是正确的,因此我猜测在这个问题下,这个不死的对象应该是“list”。
你多半其实已经有个很好的线索来怀疑这个“list”,因为这个错误消息说:
-[__NSArrayM objectAtIndex:]
这个不死的对象的类是__NSArrayM。假如你已经有一段时间的cocoa编程经验,你应该就会知道一些基本的类,就像NSString和 NSArray实际上是“class clusters”,这就意味着就像NSString或者NSArray这些原始的类在一些底层的地方会被特殊的类代替。所以在这里你可以看到一些 NSArray类型的对象,也就是这个“list”其实应该是一个NSMutableArray。
假如你却是想要确认一下,你可以增加一个NSLog()在分配了“list”数组那行代码之后:
NSLog(@"list is %p", list);
这里将会打印出和错误消息一样的内存地址(在我这里的情况下是0x6d84980,但是你自己测试的时候,地址就会不一样的)。
你也可以在调试器里面使用“p”的命令来打印出这个“list”变量的地址(和这个相对的命令就是“po”,这个命令将会打印出这个实际的对象,而不是地址)。这样方便的地方就是你可以省略很多额外增加NSLog()的步骤和从新编译这个app、
(lldb) p list
注意:非常不幸的是,上面这些命令在xcode4.3里面并没有执行的很好。由于一些原因,这个地址一直都是展示的0×00000001,可能是因为这个class cluster吧。
在GDB调试器下面,那些命令就执行的很好,在调试器的变量窗口展示出“list”都是zombie。因此我觉得这个是LLDB的bug。
为这个list 数组分配空间的地方就在initWithCoder:,就是下面这样:
list =[NSMutableArray arrayWithCapacity:10];
由于这里不是ARC(Automatic Reference Counting)(自动引用计数)项目,所以是人工管理内存,所以这里你需要retain这个变量:
// in initWithCoder: list =[[NSMutableArray arrayWithCapacity:10] retain];
为了避免内存泄露,你也不得不在dealloc函数中释放这个对象,就像下面这个:
-(void)dealloc {[list release];[super dealloc];}
再一次运行这个app。它又崩溃在这同样的一行,但是注意这个调试窗口输出的东西改变了:
Problems[8266:f803] array contents:(One,Two,Three,Four,Five)
由上面信息可以知道这个array已经分配了内存空间和包含了字符串的。这个崩溃的提示不再是EXC_BAD_ACCESS,而是SIGABRT,所以你需要再一次设置这个Exception Breakpoint。将这个解决了,继续找其他的bug!
注意:即使你使用了ARC,在这样的内存管理错误下也是一个非常大的事,你也会崩溃,得到一个EXC_BAD_ACCESS的错误,特别是假如你使用了不安全保留属性。
我的小提议:无论你什么时候得到一个EXC_BAD_ACCESS错误,你都可以开启zombie objects,然后再试试。
注意一点:你不应该一直启用zombie objects。因为这个工具将永远不会释放内存,只是简单标记一下这个内存是不死的,你最终将会在某个时候耗尽所有的内存。因此你应该在排查内存相关的错误的时候才开启zombie objects,其他时候应该关闭它。
Stepping Through the App(单步调试)
使用断点来解决这个新的问题。将断点放置在刚刚崩溃那一行:
重新运行这个程序,点击按钮。你将会在第一次执行tableView:cellForRowAtIndexPath:的时候进入调试器。注意啊,这个时候,app只是因为断点暂停了,并没有崩溃。
你想要准确的知道这个程序崩溃时的一些细节。请点击继续执行按钮,或者在(lldb)的提示后输入“c”来继续执行。程序将会从暂停的地方继续执行。
什么事情也没有发生,你仍然暂停在tableView:cellForRowAtIndexPath:这个函数的断点处。但是在调试窗口却显示:
Problems[12540:f803] array contents:(One,Two,Three,Four,Five)
这就意味着tableView:cellForRowAtIndexPath:在第一次执行的时候没有任何问题,因为NSLog()在断点之后执行了。因此这个app能够很好地创建第一个cell。
假如你键入以下的到调试提示之后:
(lldb) po indexPath
在调试窗口应该可以输出下面的:
(NSIndexPath*) $3 =0x06895680<NSIndexPath0x6895680>2 indexes [0,1]
以上重要的部分是[0, 1]。就是这个NSIndexPath对象为section 0和row 1。换句话说,这个tableview现在就在请求第二行。从这里我们可以推测这个app在第一次创建cell的时候没有任何问题,正如刚刚这里就没有发生崩溃。
多点几次这个继续按钮。在某一个特定的时候,这个程序崩溃了,并且输出一下错误消息:
Problems[12540:f803]***Terminating app due to uncaught exception 'NSRangeException', reason:'*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 4]'***Firstthrow call stack:...and so on ...
假如你检查这个indexpath对象的话,你可以看到:
(lldb) po indexPath (NSIndexPath*) $11 =0x06a8a6c0<NSIndexPath0x6a8a6c0>2 indexes [0,5]
Section依然是0,但是这个row的索引是5。注意哦,这个错误的消息也是说“index 5”。因为计数是从0开始的,当到5的时候实际上意味着已经是6的位置了。但是这里只有5项。显然这个tableview认为这里实际上有更多的行。
所以这个犯人就是下面的方法:
-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {return6;}
这个方法其实应该被写成这样的:
-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {return[list count];}
删除断点或者使断点无效,然后再次运行这个程序。终于这个tableview显示出来了,并且没有了崩溃!
注意:这个“po”命令对于检查你的对象是非常有用的。你可以在程序暂停在调试器的时候,或者在设置一个断点的时候,或者在崩溃的时候,使用这个命令。你需要确定的是这个方法当前在调用堆栈里面是高亮的,否则这个调试器将找不到这个变量。
你也可以在调试窗口的左边看到这些变量,但是就算看到了也不是很方便就能知道细节的:
Once more, with feeling
我刚刚说了没有崩溃的现象了?好,现在我们来试试滑动删除。这个app又终止了在tableView:commitEditingStyle:forRowAtIndexPath:
错误消息是:
Problems[18835:f803]***Assertion failure in-[UITableView _endCellAnimationsWithContext:],/SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:1046
这个错误看起来像是来自UIKit,并不是来自app的代码。多次输入几次“c”来让系统抛出异常,这样可以你可以得到更多有用的信息:
***Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'***Firstthrow call stack:...
经过这些,上面给你一个非常漂亮的解释。这个app告诉这个tableview里面一行要删除,但是某人却忘记从数据源里面移除这行的数据。因此这个table view看起来没有什么改变。修改这个这方法:
-(void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath {if(editingStyle ==UITableViewCellEditingStyleDelete){[list removeObjectAtIndex:indexPath.row];[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];}}
太好了,看起来这样做起效了,你终于有一个不会崩溃的app了。
Where to go from here?(何去何从)
记住下面几点:
假如你的app崩溃了,第一件事就是找到是哪里崩溃了,为什么崩溃了。一旦你知道了这两点,修复这个崩溃就很简单了。调试器可以帮助你,但是你需要知道怎么样让他帮助你。
有些崩溃可能是随机出现的,这个也是最困难的一个,特别是当你正在使用多线程。但是大多数,你可以试试,会发现一些固定的方法来让你的程序每次崩溃。
你可以想出怎么使用最少的步骤来减少崩溃的现象,这样你将找到一个好的方法来修复这个bug(也就是说他将不会发生)。但是假如你没有确定不会再生了这个错误,你就绝不能确定你的修改已经修复了这个bug。
秘诀:
1.假如崩溃在main.m里面,就可以设置全局异常断点(Exception Breakpoint)。
2.在异常断点开启的状态下,你也没有得到得到有用的信息。在这种情况下,多继续几次运行这个app,或者在调试提示后面输入“po $eax”命令。
3.大多数崩溃的一般原因和一些bug都是在你的xib中或者storyboard中的连接丢失了或者是错误的连接。这些情况不会在编译错误里面显示,因此你一般不知道。
4.不要忽略编译警告。假如你有编译警告,就说明你有些东西可能会出错。假如你不知道为什么你会到一个编译警告,最好去搞明白它. 这些都是安全的做法!
5.在设备上调试可能会和在模拟器上面有些微的不同。这两个环境不是完全一样,你将会得到不同的结果。
例如,当你运行一个有问题的程序在iphone4上的时候,这第一个崩溃就会发生在NSArray初始化的时候,因为你缺少一个nil标记,而不是会因为当这个app执行setList:的时候的时候崩溃。所以说上面那个原则方法就可以帮你找到崩溃问题的根源本质。
不要忘记静态分析工具(static analyzer tool),这个工具将会捕获更多的错误。假如你是一个初学者,推荐你开启它。你可以在Build Settings界面上为你的工程设置:
调试愉快吧!