iOS 程序Bug调试方法

欢迎回到当程序崩溃的时候怎么办 教程!

在这个教程的第一部分,我们介绍了SIGABRT和EXC_BAD_ACCESS错误,并且举例说明了一些使用xcode调试器(Xcode debugger)和异常断点(Exception Breakpoints)解决问题的策略。

但是我们的app仍然有一些问题!就像我们看到的,他工作的并不是很好,并且这里仍然有许多潜在的可能崩溃的问题。

幸运的是,在这个教程的第二部分,也是最后一部分,我们可以学习更多的技术来处理这些问题。

所以我们就不在啰嗦了,让我们回到继续修正这个充满bug的app中吧!

Learn how to debug and fix dreaded app crashes!

Getting Started: When What’s Supposed to Happen, Doesn’t

 

在第一部分我们停止的地方,经过许多的调试工作之后,我们运行这个程序他是不会崩溃的。但是他却展现了一个没有预料到的空的table,就像下面一样:

The table view doesn't show any rows.

当你觉得一些事情应该发生,但是却没有发生的时候,这里有些你可以使用一些技巧来排除问题。在这个教程里面,我们首先是学习使用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类。

Setting the class for the table view controller.

由上图我们可以看出,在身份检查器(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:这些名字的方法。

The call stack for the assertion failure on the table view.

继续运行这个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必须有重用的标识)”。这个是不能忽视的警告:

Xcode warns about a missing prototype cell identifier.

打开storyboard,选择这个标准的cell(在tableview的顶端,并且显示的是“Title”的单独的一个cell),并且设置cell的标识符为“Cell”:

Giving the prototype cell a reuse identifier.

将那个修复了之后,所以的编译警告应该没有了。运行这个app,现在这个调试窗口应该会打印出来:

Verify Your Assumptions

你的NSLog()打印出来的消息,已经告诉我们6个table view cell被创建了,但是在table上面什么都看不见。怎么回事呢?假如你在模拟器里面到处点击一下,你将会注意到tableview中6个cell中的第一个却能够被选中。所以,显然cells都是存在的,只是他们都是空的:

The table appears empty but cells can actually be selected.

是时候需要更多的调试记录了。将先前的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)了。你其实也可以增加其他的断点,并且可以放置到代码的任何地方。一旦你的程序运行到断点的地方,这个断点就会被触发,并且程序就会进入调试模式。

你可以通过点击代码编辑区前面的行号来放置特殊的断点:

Setting a breakpoint on a line of code.

这个蓝色的箭头所指示的那一行就有一个断点了。你也可以在断点导航器(Breakpoint Navigator)里面看到这个新的断点:

The new breakpoint in the Breakpoint Navigator.

再一次运行这个app。假如initWithStyle:确实是会被调用的话,那么你点击了“Tap Me!”按钮之后,当这个ListViewController被装载的时候,这个app将会暂停,并且会进入调试器。

可能正如你所料的,什么事情也没有发生。initWithStyle:没有被调用。其实这个是可以讲得通的,因为view controller是从storyboard(或者xib)中装载的,所以使用的应该是initWithCoder:方法。

将之前initWithStyle:方法替换为initWithCoder::

并且保持断点在这个方法上面,来看看它是怎么工作的:

Setting the breakpoint on initWithCoder.

一旦你点击了那个按钮,这个app将会进入调试器:

The debugger is paused on the breakpoint.

以上的情况并不是意味着这个app崩溃了!它只是在这个断点处暂停了。在左边的执行堆栈里面(假如你没有看到执行堆栈的话,你可能需要切换到调试导航器),你可以看到你是从buttonTapped:到这里的。这个调试导航器里面,我们看到执行了一系列的UIKit的方法,并且装载了一个新的view controller。(顺便说句,断点是一个非常好的工具来指出这个系统是怎么工作的。)

如果想要离开你之前停留的地方,继续运行这个程序,简单的就是点击继续程序运行按钮,或者在调试控制台中输入“c”。

显然的是,一切并没有如我们料想的一样,这个app又奔溃了。我告诉过你,它有很多bug的。

注意:在你继续之前,在initWithCoder:移除断点或者使断点无效。因为他已经展现了他的目的,所以现在它可以离开了。

你可以在显示行号的的地方右击断点,并且在弹出的菜单中选择删除断点。你也可以拖出这个断点离开窗口,或者在断点调试器里面移除。

假如你并不想移除这个断点,你可以简单的使断点无效。为了达到这个目的,你可以使用右击弹出菜单,或者左击一次这个断点。判断这个断点是否有效,你可以看看这个断点的颜色,当为浅蓝色了就是无效了,深蓝色就是有效的。

Zombies!

回到这个崩溃。它是一个EXC_BAD_ACCESS,幸运的是调试器指到了他发生在那里,在tableView:cellForRowAtIndexPath:

EXC_BAD_ACCESS error on cellForRowAtIndexPath.

这是一个EXC_BAD_ACCESS崩溃,意味着在你的内存管理里面有bug。不像SIGABRT,你将不会得到很明朗的错误消息。然而你可以使用一个让你看到曙光的调试工具:Zombies!

打开这个项目的scheme editor:

The Edit Scheme menu option.

选择Run 选项,然后选择Diagnosics标签。勾上Enable Zombie Objects选项:

Enabling the Zombie Objects diagnostic option.

现在运行这个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。

The GDB debugger points out which object is the zombie.

为这个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(单步调试)

使用断点来解决这个新的问题。将断点放置在刚刚崩溃那一行:

Setting the breakpoint on cellForRowAtIndexPath.

重新运行这个程序,点击按钮。你将会在第一次执行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”命令对于检查你的对象是非常有用的。你可以在程序暂停在调试器的时候,或者在设置一个断点的时候,或者在崩溃的时候,使用这个命令。你需要确定的是这个方法当前在调用堆栈里面是高亮的,否则这个调试器将找不到这个变量。

你也可以在调试窗口的左边看到这些变量,但是就算看到了也不是很方便就能知道细节的:

The debugger shows the content of your variables.

Once more, with feeling

我刚刚说了没有崩溃的现象了?好,现在我们来试试滑动删除。这个app又终止了在tableView:commitEditingStyle:forRowAtIndexPath:

Swipe-to-delete will make the app crash.

错误消息是:

这个错误看起来像是来自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界面上为你的工程设置:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值