转自:http://www.ityran.com/archives/1143编程
------------------------------------------------数组
欢迎回到当程序崩溃的时候怎么办 教程!xcode
在这个教程的第一部分,咱们介绍了SIGABRT和EXC_BAD_ACCESS错误,而且举例说明了一些使用xcode调试器(Xcode debugger)和异常断点(Exception Breakpoints)解决问题的策略。安全
可是咱们的app仍然有一些问题!就像咱们看到的,他工做的并非很好,而且这里仍然有许多潜在的可能崩溃的问题。多线程
幸运的是,在这个教程的第二部分,也是最后一部分,咱们能够学习更多的技术来处理这些问题。app
因此咱们就不在啰嗦了,让咱们回到继续修正这个充满bug的app中吧!框架
Getting Started: When What’s Supposed to Happen, Doesn’tiphone
在第一部分咱们中止的地方,通过许多的调试工做以后,咱们运行这个程序他是不会崩溃的。可是他却展示了一个没有预料到的空的table,就像下面同样:ide
当你以为一些事情应该发生,可是却没有发生的时候,这里有些你可使用一些技巧来排除问题。在这个教程里面,咱们首先是学习使用NSlog来解决这个问题。函数
这个table view controller的类是ListViewController。在一系列的任务执行以后,这个app应该装载ListViewController,而且在屏幕上面显示出来。你能够作一个测试,来肯定view controller的方法是执行了的。因此viewDidLoad这个方法看起来应该是一个好地方来作测试。
在ListViewController.m,增长一个NSLog()到viewDidload,就像下面同样:
当你运行这个app时,你应该指望当咱们点击了“Tap Me”按钮后在调试窗口看到“viewDidLoad is called”这样文字。如今就来试试,点都不惊讶,在调试窗口什么也没有出现。那就意味着ListViewController类根本没有被使用!
这个多半意味着,你可能忘记了告诉storyboard你想要为table view controller场景使用ListViewController类。
由上图咱们能够看出,在身份检查器(Identity Inspector)的类属性区域是设置的默认值UITableViewController。改变这个Custom Class下面的class为ListViewController,而后再一次运行这个app。如今在调试窗口应该就会出现“viewDidLoad is called”文字:
可是此次app将会再一次崩溃,可是倒是一个新的问题。
注意:一旦你的代码好像没起什么什么做用的话,放置一些NSLog()在确切的地方,来看看是否这个方法是被执行了的和cpu经过怎么样路径执行这个方法。使用NSLog()来测试你假设将会执行的代码。
Assertion Failures
这个新的有趣的崩溃。它是一个SIGABRT,而且在调试窗口打印出来的是如下消息:
咱们获得的是一个执行UITableView的一些方法的一个“断言错误(assertion failure)”。当某些东西出错了以后,一个断言是一个内部相容性的检查器,而且会抛出一个异常。你也能够放置断言在你的代码里。例如:
在上面的方法里面,咱们让一个NSString对象做为这个函数的变量,可是代码却不容许调用者传递一个nil或者长度小于3的字符串。假如这些条件中的一个不匹配的话,这个app将会终止,而且抛出一个异常。
你可使用断言来做为一个防护性编程技术,所以你应该肯定这个就是咱们想要的代码行为。断言一般只在调试编译下有用的,所以他们对发布到app store的最终的app是没有运行时的影响的。
在这个状况下,某些状况触发了一个UITableView的断言错误,可是你并无彻底肯定在那个地方。App也是中止在main.m里面,而且在执行堆栈里面只包含了框架(framework)的方法。
从这些方法的名字,咱们能够猜想这个错误发生在重画这个tableview的某些地方。例如,咱们能够看到layoutSubviews和_updateVisibleCellsNow:这些名字的方法。
继续运行这个app来看看是否能够获得一些比较好的错误消息—–记住,如今只是在抛出异常的时候暂停了程序,并无崩溃。点击继续程序按钮,或者在调试窗口键入下面的命令:
你可能不得很少点击几回继续按钮,“c”命令也是一个简短的继续指令,和点击继续按钮一个效果,并非就直接执行到最后。
如今这个调试窗口喷发出一些比较有用的信息:
太好了,这是一个至关好的一个线索。显然这个UITableView的数据源没有从tableView:cellForRowAtIndexPath:方法返回一个有效的cell,所以在ListViewController.m方法里面增长一些调试输出信息来看看:
你增长一个NSLog()标记。再一次运行这个app,看看输出了什么:
从以上信息咱们能够看出,调用dequeueReusableCellwithIdentifier:返回的倒是nil,这就意味着使用“Cell”做为标识符的cell可能不存在(由于这个app使用的是标准的cell的storyboard)。
固然,这也是愚蠢的bug,而且毫无疑问的是,在之前解决这个须要很长的时间,可是如今却不是了,由于xcode已经经过静态编译警告了你:“Prototype cells must have reuse identities。(标准的cell必须有重用的标识)”。这个是不能忽视的警告:
打开storyboard,选择这个标准的cell(在tableview的顶端,而且显示的是“Title”的单独的一个cell),而且设置cell的标识符为“Cell”:
将那个修复了以后,因此的编译警告应该没有了。运行这个app,如今这个调试窗口应该会打印出来:
Verify Your Assumptions
你的NSLog()打印出来的消息,已经告诉咱们6个table view cell被建立了,可是在table上面什么都看不见。怎么回事呢?假如你在模拟器里面处处点击一下,你将会注意到tableview中6个cell中的第一个却可以被选中。因此,显然cells都是存在的,只是他们都是空的:
是时候须要更多的调试记录了。将先前的NSLog()标记改变一下:
如今你打印出来就是你的数据模块的内容。运行这个app,看看显示出来的是什么:
上面的很好的解释了为何在cell里面什么都没有看到的缘由:由于这个文字(text)始终是nil。然而,假如你检查你的代码,而且在initWithStyle:方法里面显示的添加了不少的字符串到list array里面:
就像上面那样,这是测试你的假设是否是正确的一个很好的方法。可能你还想更准确的看看这个array里面到底有什么东西。改变先前在tableView:cellForRowAtIndexPath:里面的NSLog()为这样:
至少这样能够给你展现一些东西。运行这个app。假如你还没准备好猜想会发生什么状况,调试窗口已经给你打印出来了:
哈哈,你的脸色瞬间阴沉下来。上面的代码竟然没有起做用,由于你可能忘了在首先为这个array对象申请内存空间。这个“list”因此一直为nil,所以调用addObject: 和objectAtIndex:不会起任何的做用。
你应该在你的view controller被装载的时候为这个list对象分配空间,所以在initWithStyle:方法里面应该是一个不错的选择。修改那个方法为:
试一试。我晕,依然什么都没有!调试窗口输出依然是:
通过了这么多假设和修改,可是仍是什么都没有,这些真的是很是使人沮丧啊,可是请记住你可能会一直继续到最后,直到你弄清楚了全部的假设。因此如今的问题就是难道initWithStyle:没有被调用?
Working With Breakpoints
你可能又会在代码里面放置另一个NSLog()标志,可是其实你彻底可使用另外的工具:断点( breakpoints)。你已经看到过不管何时只要有异常抛出的时候,程序就会终止的异常断点(Exception Breakpoint)了。你其实也能够增长其余的断点,而且能够放置到代码的任何地方。一旦你的程序运行到断点的地方,这个断点就会被触发,而且程序就会进入调试模式。
你能够经过点击代码编辑区前面的行号来放置特殊的断点:
这个蓝色的箭头所指示的那一行就有一个断点了。你也能够在断点导航器(Breakpoint Navigator)里面看到这个新的断点:
再一次运行这个app。假如initWithStyle:确实是会被调用的话,那么你点击了“Tap Me!”按钮以后,当这个ListViewController被装载的时候,这个app将会暂停,而且会进入调试器。
可能正如你所料的,什么事情也没有发生。initWithStyle:没有被调用。其实这个是能够讲得通的,由于view controller是从storyboard(或者xib)中装载的,因此使用的应该是initWithCoder:方法。
将以前initWithStyle:方法替换为initWithCoder::
而且保持断点在这个方法上面,来看看它是怎么工做的:
一旦你点击了那个按钮,这个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仍然崩溃,可是如今你将会获得下面的错误消息:
上面这个就是zombie enable 工具所作的,作个小归纳:不管何时你建立了一个新对象(经过发送“alloc”消息),一块内存将会为这个对象的实例变量保留。当这个对象被释放,他的保留计数(retain count)变成0,这块内存将会被释放。
可是,你可能仍然有许多的指针指向这个已经失效的内存,这些都是创建在假设这里有一个有效的对象存在的状况下。假如你程序的某些部分试着使用这个野指针,这个app将会伴随着EXC_BAD_ACCESS的错误崩溃掉。
(假如你是很幸运的话,这个程序将会崩溃。假如你没那么幸运的哈,这个app将会使用这个死亡的对象,各类各样的破坏可能相继发生,特别是某个指针所指向的这个内存区域已经被一个新的对象从新分配了。)
当这个zombie工具被启用以后,即便这个对象被释放了,这个对象的内存也不会被清理。因此,那块内存将会被标记为“长生不死的”。假如你试着以后又去使用这块内存,这个app可以意识到你的错误操做,而且app将会抛出“message sent to daellocated instance”错误而且终止运行。
所以这就是以前发生的事。这行就是使用了不死的对象:
这个cell对象和他的textLabel应该是好的,那么indexPath也应该是正确的,所以我猜想在这个问题下,这个不死的对象应该是“list”。
你多半其实已经有个很好的线索来怀疑这个“list”,由于这个错误消息说:
这个不死的对象的类是__NSArrayM。假如你已经有一段时间的cocoa编程经验,你应该就会知道一些基本的类,就像NSString和NSArray其实是“class clusters”,这就意味着就像NSString或者NSArray这些原始的类在一些底层的地方会被特殊的类代替。因此在这里你能够看到一些NSArray类型的对象,也就是这个“list”其实应该是一个NSMutableArray。
假如你倒是想要确认一下,你能够增长一个NSLog()在分配了“list”数组那行代码以后:
这里将会打印出和错误消息同样的内存地址(在我这里的状况下是0x6d84980,可是你本身测试的时候,地址就会不同的)。
你也能够在调试器里面使用“p”的命令来打印出这个“list”变量的地址(和这个相对的命令就是“po”,这个命令将会打印出这个实际的对象,而不是地址)。这样方便的地方就是你能够省略不少额外增长NSLog()的步骤和重新编译这个app、
注意:很是不幸的是,上面这些命令在xcode4.3里面并无执行的很好。因为一些缘由,这个地址一直都是展现的0×00000001,多是由于这个class cluster吧。
在GDB调试器下面,那些命令就执行的很好,在调试器的变量窗口展现出“list”都是zombie。所以我以为这个是LLDB的bug。
为这个list 数组分配空间的地方就在initWithCoder:,就是下面这样:
因为这里不是ARC(Automatic Reference Counting)(自动引用计数)项目,因此是人工管理内存,因此这里你须要retain这个变量:
为了不内存泄露,你也不得不在dealloc函数中释放这个对象,就像下面这个:
再一次运行这个app。它又崩溃在这一样的一行,可是注意这个调试窗口输出的东西改变了:
由上面信息能够知道这个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:这个函数的断点处。可是在调试窗口却显示:
这就意味着tableView:cellForRowAtIndexPath:在第一次执行的时候没有任何问题,由于NSLog()在断点以后执行了。所以这个app可以很好地建立第一个cell。
假如你键入如下的到调试提示以后:
在调试窗口应该能够输出下面的:
以上重要的部分是[0, 1]。就是这个NSIndexPath对象为section 0和row 1。换句话说,这个tableview如今就在请求第二行。从这里咱们能够推测这个app在第一次建立cell的时候没有任何问题,正如刚刚这里就没有发生崩溃。
多点几回这个继续按钮。在某一个特定的时候,这个程序崩溃了,而且输出一下错误消息:
假如你检查这个indexpath对象的话,你能够看到:
Section依然是0,可是这个row的索引是5。注意哦,这个错误的消息也是说“index 5”。由于计数是从0开始的,当到5的时候实际上意味着已是6的位置了。可是这里只有5项。显然这个tableview认为这里实际上有更多的行。
因此这个犯人就是下面的方法:
这个方法其实应该被写成这样的:
删除断点或者使断点无效,而后再次运行这个程序。终于这个tableview显示出来了,而且没有了崩溃!
注意:这个“po”命令对于检查你的对象是很是有用的。你能够在程序暂停在调试器的时候,或者在设置一个断点的时候,或者在崩溃的时候,使用这个命令。你须要肯定的是这个方法当前在调用堆栈里面是高亮的,不然这个调试器将找不到这个变量。
你也能够在调试窗口的左边看到这些变量,可是就算看到了也不是很方便就能知道细节的:
Once more, with feeling
我刚刚说了没有崩溃的现象了?好,如今咱们来试试滑动删除。这个app又终止了在tableView:commitEditingStyle:forRowAtIndexPath:
错误消息是:
这个错误看起来像是来自UIKit,并非来自app的代码。屡次输入几回“c”来让系统抛出异常,这样能够你能够获得更多有用的信息:
通过这些,上面给你一个很是漂亮的解释。这个app告诉这个tableview里面一行要删除,可是某人却忘记从数据源里面移除这行的数据。所以这个table view看起来没有什么改变。修改这个这方法:
太好了,看起来这样作起效了,你终于有一个不会崩溃的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界面上为你的工程设置:
调试愉快吧!