我的应用程序崩溃,现在怎么办? - 第2部分
也可以在这个职位是:西班牙语
这是后由iOS教程的团队成员Matthijs Hollemans,经验丰富的iOS开发者和设计师。谷歌和Twitter上,你可以找到他。
欢迎回到调试教程!
本教程的第一部分介绍了了SIGABRT EXC_BAD_ACCESS错误,并说明使用Xcode调试器和异常断点解决这些问题的一些策略。
但是,我们的应用程序仍然有一些问题!这是行不通的,正是因为它,有很多的崩溃仍然潜伏。
幸运的是,有更多的技巧,你可以学习如何处理这些问题,我们将在这本系列教程的第二个和最后部分。
因此,事不宜迟,让我们马上回固定这个车的应用程序!
入门:什么是应该发生的,不
我们离开的地方的应用程序跑在第一部分中,经过大量的调试工作没有崩溃。但它显示了一个出人意料的空表,就像这样:
当你期待发生什么事情,但它没有,也有一些技巧,你可以用它来 排除故障。本教程将首先NSLog的()来处理这个问题。
的类表视图控制器是ListViewController。赛格瑞后,应用程序应该的ListViewController加载,并在屏幕上显示其观点。您可以测试假设确保视图控制器的方法实际上是被称为viewDidLoad中好像是一个好地方。
ListViewController.m viewDidLoad中,添加一个NSLog的()如下:
|
当您运行的应用程序,你应该会看到文本“viewDidLoad中被称为”调试窗格中,按下后点选我!按钮。试试吧。这并不奇怪,没有出现在调试窗格中。这意味着在所有不使用的ListViewController类!
这通常意味着,你可能忘了告诉你想使用该表视图控制器现场的ListViewController类的故事情节。
没错,在Identity督察级字段设置为默认值,UITableViewController的。将其更改为ListViewController并再次运行应用程序。现在的“viewDidLoad中被称为”文字会出现在调试输出:
Problems[18375:f803] You tapped on: <UIRoundedRectButton: 0x6894800; frame = (119 189; 82 37); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x68948f0>> Problems[18375:f803] viewDidLoad is called |
此外,应用程序将再次崩溃,但是,这是一个新的问题。
注意:每当你的代码似乎并没有做任何事情,放置几个NSLog的()语句战略的地方,一定的方法是否实际被称为CPU需要通过这些方法路径。使用NSLog的()来测试你的代码做什么假设。
断言失败
这种新的崩溃是一个有趣的。这是一个SIGABRT的的调试窗格说以下:
Problems[18375:f803] *** Assertion failure in -[UITableView _createPreparedCellForGlobalRow: withIndexPath:], /SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:6072 |
我们有一个“断言失败”,有事情做一个UITableView。一个说法是内部一致性检查,抛出一个异常,什么是错的。你可以把自己的代码,断言。例如:
|
上述方法需要一个NSString对象作为它的参数,但代码不会允许呼叫者传递零或少于三个字符的字符串。如果这些条件得不到满足,该应用程序将中止与异常。
你使用断言作为防守的编程技术,使你总是确保代码的行为预期。通常只在调试版本中启用断言,所以他们有分布在App Store上的应用程序,是在最后没有运行的影响。
在这种情况下,这引发一个断言失败的UITableView,但你哪里还不能完全肯定。该应用程序已暂停main.m和调用堆栈只包含框架方法。
从这些方法的名称,你可以猜测,这个错误有事情做,重绘表视图-例如,我看到名为“ 的layoutSubviews_updateVisibleCellsNow的“方法:。
继续运行的应用程序,如果你要得到一个更好的错误消息 - 还记得,你只是将抛出异常之前,目前暂停。按继续执行程序“按钮,或调试窗格中键入以下内容:
( LLDB ) C |
您可能有两次做到这一点。“c”的命令是继续做完全相同的东西继续执行程序按钮。
现在调试窗格中吐出了一些有用的信息:
|
所有的权利,这是一个很好的提示。显然UITableView的数据源没有返回一个有效的细胞的tableView:cellForRowAtIndexPath: 。所以添加一些调试输出,在ListViewController.m方法如下:
|
你添加一个NSLog的()语句。再次运行应用程序,看看它说。
Problems[18420:f803] the cell is (null) |
OK,这样,这意味着调用dequeueReusableCellWithIdentifier的:回到零,只发生在标识符为“细胞”的细胞不能被发现(因为应用程序使用一个故事情节与原型细胞)的东西。
当然,这是一个愚蠢的错误,你毫无疑问已经解决了前很长一段时间,因为Xcode中已经警告通过方便的编译器的警告:“原型细胞必须有重用标识符。”我不告诉你忽略这些警告!:P
打开脚本,选择原型细胞(单细胞在上面的表说,“标题”),和设置标识符细胞:
与固定,所有编译器警告应该走了。再次运行应用程序,现在在调试窗格应该说:
|
Problems[7880:f803] the cell is <UITableViewCell: 0x6a6d120; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6a6d240>> Problems[7880:f803] the cell is <UITableViewCell: 0x6877620; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6867140>> Problems[7880:f803] the cell is <UITableViewCell: 0x6da1e80; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6d9fae0>> Problems[7880:f803] the cell is <UITableViewCell: 0x6878c40; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6878f60>> Problems[7880:f803] the cell is <UITableViewCell: 0x6da10c0; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6d9f240>> Problems[7880:f803] the cell is <UITableViewCell: 0x6879640; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6878380>>
验证你的假设
NSLog的()表明,六表视图创建细胞,表中可见,但仍然没有。是什么原因?好吧,如果你周围点击模拟器有点,你会发现,前六个表视图中的细胞,其实是可以选择现在。显然,细胞是有,但他们只是空:
一些更多的调试日志记录的时间。更改您以前NSLog的()语句:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
cell.textLabel.text = [list objectAtIndex:indexPath.row];
NSLog(@"the text is %@", [list objectAtIndex:indexPath.row]);
return cell;
}
|
现在,你登录你的数据模型的内容。运行的应用程序,并检查出什么,它说:
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)
|
这就解释了为什么没有显示在细胞:因为文本始终是零。但是,如果检查的代码,在顶部的在initWithStyle类:你肯定是把字符串到列表数组:
|
在这样的情况下,它总是一个好主意,再次测试你的假设。也许你应该看到里面究竟是您的阵列。NSLog的()的tableView:cellForRowAtIndexPath:更改为:
|
这至少应该告诉你的东西。再次运行该应用程序。如果不是你已经猜到了,调试输出是:
[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) |
啊哈!一个灯泡熄灭在你的头上。这是从来没有去上班,因为有人忘了摆在首位,实际分配的数组对象。“名单”伊娃总是为零,所以调用的AddObject:objectAtIndex:从来没有任何效果。
您应该分配列表对象时,您的视图控制器被加载,所以里面initWithStyle:似乎是个好地方。改变这种状况的方法:
|
给一个尝试。哎呀,仍然一无所获!的调试输出再次说:
问题[ 7971:F803 ]数组的内容:(空) 。。。等。。。 |
这种事情是相当令人沮丧,但请记住,你最终得到这条底线,如果你已经验证所有假设你已经。所以现在要问的问题是,initWithStyle:实际上被称为?
使用断点
你可以把另一个NSLog的()语句中的代码,但还有另外一个工具,你可以使用:断点。您已经看到了异常断点,暂停的应用程序时,抛出一个异常。您还可以添加其他断点,在你的代码中的几乎任何地方。程序只要击中,当场被触发,断点和应用程序跳转到调试器。
你可以在你的代码在特定线路上设置一个断点,点击行号:
蓝色箭头表示,现在这条线有断点。您还可以看到这个新的断点,断点导航:
再次运行该应用程序。如果initWithStyle:确实叫,然后应用程序应暂停和跳跃进入调试后,挖掘“轻点!”按钮,加载时ListViewController。
正如你可能已经预期,没有这样的事情发生。initWithStyle:不会被调用。这是有道理的,当然,因为加载视图控制器从情节板(或笔尖),在这种情况下,initWithCoder:方法是用来代替。
更换initWithStyle:
|
这种方法保持断点,正好看到这是如何工作的:
只要你轻点按钮,应用程序跳转到调试器:
这并不意味着该应用程序已经崩溃了!它只是暂停在断点的位置。在左边的调用堆栈(如果你没有看到的调用堆栈,您可能需要切换到调试导航器),你可以看到,你有从这里buttonTapped: 。中的所有方法之间的代码是UIKit的调用来执行一个SEGUE和加载一个新的视图控制器。(顺便说一下,断点是一个伟大的工具,弄清楚如何系统的内部工作原理。)
从你离开的地方继续运行的应用程序,只需轻按程序继续执行按钮或在调试控制台中的“C”型。
当然,这不会去预期和应用程序再次崩溃。我告诉你,这是一个有点越野车!
注:在继续之前,这是一个好主意,删除或禁用断点利用initWithCoder: 。它已成为它的目的,所以现在它可以走。
为此,您可以通过右键单击在阴沟里的断点(面积在文本编辑器了行号),从弹出菜单中选择“删除断点。您也可以拖动断点的窗口,你可以删除它从断点导航。
如果你不想删除断点,只是还没有,你可以简单地将其禁用。要做到这一点,你可以使用右键菜单中,或者你可以点击一次的断点 - 如果断点较淡蓝色指示灯亮起,它的禁用。
但是,还有另一种常见的错误的initWithCoder:方法。你能找到它吗?
僵尸!
备份的崩溃。这是一个EXC_BAD_ACCESS,幸运的是,调试器指向它发生在哪里,里面的tableView:cellForRowAtIndexPath:
它是一个EXC_BAD_ACCESS崩溃意味着有一个错误在你的记忆管理。SIGABRT不同的是,你不会得到一个友好的错误消息,这样的崩溃。然而,有一个调试工具可以使用,可能在这里发生了什么事情,有一些启发:植物大战僵尸!
打开项目的方案编辑器:
选择运行的动作,然后“诊断”选项卡。检查启用的僵尸对象框:
现在,再次运行应用程序。该应用程序仍然崩溃,但现在你会得到以下错误消息:
问题18702:F803 ] *** - [ __NSArrayM objectAtIndex:]:传送释放实例0x6d84980的 |
以下是的僵尸启用工具呢,一言以蔽之:每当你创建一个新的对象通过发送一个“黄金”的消息,一大块的内存被保留,以保持该对象的实例变量。当对象被释放,其保留计数降为零,该内存被释放,以便其他对象可以使用它在未来。到目前为止,一切都不错。
然而,这是可能的,你仍然有指针指向的假设下,现在已不存在的内存块,仍然是一个有效的对象。如果尝试使用一些程序的一部分,陈旧的指针,应用程序将崩溃EXC_BAD_ACCESS错误。
(它会崩溃,至少,如果你是幸运的,如果你运气不好,应用程序将使用的死对象和各种混乱可能会随之而来,尤其是如果内存在某些时候得到一个新的对象覆盖)。
僵尸工具启用时,对象的存储器,用于不浏览释放时,对象被释放。相反,该内存被标为“亡灵”,如果您尝试稍后再访问这些内存,应用程序可以承认自己的错误,它会中止“消息发送到释放实例”错误。
所以,这就是发生在这里。这是线不死对象:
|
的细胞对象及其textLabel的可能是不错的,所以indexPath,所以我的猜测是,亡灵这里是有问题的对象“列表中。”
您已经有一个相当不错的提示,它的确是“名单”,因为错误消息说:
- [ __NSArrayM objectAtIndex:] |
该类的亡灵对象的是__ NSArrayM。如果你一直与可可编程了一段时间,你知道一些,如NSString和NSArray的基金会类的,其实是“类集群,”这意味着原班 - 的NSString或NSArray的 - 一个特殊的内部类被替换。所以在这里,你可能看一些NSArray的类型的对象,而这正是“名单”(一个NSMutableArray)。
如果你想确保,你可以添加一个NSLog的()线后,“列表”数组分配:
|
这应该打印错误消息中(在这种情况下0x6d84980相同的内存地址,但是当你尝试它的地址会有所不同)。
你也应该能够使用“p”命令从调试器打印出来的地址“列表”变量(而不是“PO”的命令,打印出实际的对象,而不是它的地址)。这样可以节省你从NSLog的()语句中添加和重新编译应用程序的额外步骤。
( LLDB ) P表 |
注:不幸的是,这似乎并没有正常工作,对我来说和Xcode 4.3。出于某种原因,总是显示的地址为0×00000001,可能是因为类簇。
然而,随着GDB调试器,工作正常,在调试器中的“变量”窗格中甚至指出,“名单”是僵尸。所以我假定这是一个错误LLDB。
initWithCoder:方法列表数组中的分配,目前看起来是这样的:
|
因为这不是一个ARC(自动引用计数)项目 - 它使用手动内存管理 - 你需要保留这个变量:
|
为了防止内存泄漏,你还必须在dealloc释放对象如下:
|
再次运行该应用程序。它仍然在同一行崩溃,但注意到,调试输出已经改变:
Problems[8266:f803] array contents: ( One, Two, Three, Four, Five ) |
这意味着该阵列是否已被正确地分配和它包含的字符串。崩溃也不再是一个EXC_BAD_ACCESS,但SIGABRT,你再次挂在异常断点。解决一个问题,找到另一个。: - ]
注:即使这样的内存管理相关的错误与ARC很大程度上是过去的事情,你仍然可以使你的代码崩溃EXC_BAD_ACCESS错误,特别是如果你使用的unsafe_unretained性能和冬麦区。
我的提示:每当你得到一个EXC_BAD_ACCESS错误,使僵尸对象,然后再试一次。
请注意,你不应该离开僵尸对象使所有的时间。因为这个工具不会释放内存,但只是简单的标记作为亡灵,你最终泄漏所有的地方,在某些时候会耗尽可用内存。所以只能让僵尸对象来诊断内存相关的错误,然后再次禁用它。
通过应用程序步进
使用断点弄清楚这个新问题。把它放在崩溃的行:
再次运行应用程序,然后点击按钮。现在,您将跳转到调试器的第一时间的tableView:cellForRowAtIndexPath:被调用。请注意,在这一点上的应用程序还没有崩溃,它只是暂停。
你想弄清楚什么时候该应用程序崩溃。按程序继续执行“按钮或”C“型的(LLDB)提示后面。这将恢复从您停止的程序。
没有什么可能出现已经发生-你还在泰伯维的:cellForRowAtIndexPath: -但现在调试窗格中显示:
Problems[12540:f803] array contents: ( One, Two, Three, Four, Five ) |
这意味着的tableView:cellForRowAtIndexPath:没有执行一次,没有任何问题,因为这NSLog的()语句断点后发生。因此,应用程序能够创建的第一个单元格就好了。
如果您键入以下到调试提示:
( LLDB ) PO indexPath |
那么输出应该是这样的:
( NSIndexPath * ) $ 3 = 0x06895680 NSIndexPath 0x6895680 > 2的指标[ 0,1 ] |
最重要的部分是[0,1]。此NSIndexPath对象显然是第0,第1行。换句话说,表视图寻求第二行。由此看来,我们可以得出结论,该应用程序有没有问题,创建的第一行的单元格,偶们没有发生。
按继续执行程序按钮若干次。在某一点上,应用程序崩溃,并显示以下消息:
|
如果您检查indexPath对象现在,你会看到:
( LLDB ) PO indexPath ( NSIndexPath * ) $ 1 1 = 0x06a8a6c0 < NSIndexPath 0x6a8a6c0 > 2指标[ 0 ] |
部分指数仍然是0,但该行的索引为5。请注意该错误消息也说:“索引5”,因为从0开始计数,实际上意味着指数5排第六。但也有仅在数据模型中的五个项目!显然,表观点认为有更多的行比实际上有。
当然是罪魁祸首,下面的方法:
|
它确实应该写为:
|
删除或禁用断点,并再次运行应用程序。最后,表视图显示,有没有更多的崩溃!
注: “PO”命令检查你的对象是非常有用的。您可以使用它时,你的程序在调试器暂停后,无论遇到断点或已经崩溃后。你确实需要,以确保正确的方法是在调用堆栈中突出显示,否则调试器将不能够找到变量。
您还可以看到这些变量在调试器的左窗格中,但往往你所看到的,有可能需要一点点破译弄清楚:
一旦有了感觉
我说,没有更多的崩溃?嗯,差不多......尝试轻扫删除。现在的应用程序实现代码如下:commitEditingStyle:forRowAtIndexPath终止。
错误消息是:
问题[ 18835:F803 ] ***断言失败- 的UITableView _endCellAnimationsWithContext:/ SourceCache / UIKit_Sim / UIKit的1912.3 / UITableView.m:1046 |
这看起来像是来自UIKit的,而不是从应用程序的代码。“C”型几次抛出异常,所以你会得到一个更为有用的错误消息:
***终止应用程序到未捕获异常NSInternalInconsistencyException的由于' 原因:“无效的更新:在第0行数无效。的行数 包含在更新后(5)在现有的部分的数量必须等于 更新前(5)在这一节中所载的行,加上或减去数 (0插入,删除1)该节中插入或删除的行,加上或 减去移入或移出该节的行数(0移动,0迁出)。 ***首先抛出调用堆栈:。。。 |
啊,你去那里。这是不言自明。该应用程序告诉表视图,行已被删除,但有人忘了从数据模型中删除。因此,表视图中不会看到任何变化。修复的方法,内容如下:
|
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[list removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
很棒!它采取了一些努力,但你终于有了一个崩溃的应用程序。: - ]
从这里去哪里?
一些关键点要记住:
如果您的应用程序崩溃,第一件事是要弄清楚它的确切位置坠毁原因。一旦你知道这两样东西,往往容易固定崩溃。调试器可以帮助你,但你需要了解如何让它为你工作。
有些崩溃似乎是随机发生的,而这些都是艰难的,尤其是当你正在使用多个线程。然而,在大多数的时间里,你可以找到一种一致的方式,使您的应用程序崩溃,每次尝试。
如果你能弄清楚如何用最少的步数重现崩溃,那么你也将有一个很好的方式来验证,错误是固定的(也就是说,它不会再次发生)。但是,如果你不能可靠地重现错误,那么你永远不能肯定你的变化,它走。
温馨提示:
- 如果应用程序崩溃main.m,然后设置异常断点。
- 启用异常断点,你可能不再获得有用的错误消息。在这种情况下,要么继续,直到你做的应用程序,或调试提示符后键入“宝$ eax中”命令。
- 如果你得到一个EXC_BAD_ACCESS,使僵尸对象,然后再试一次。
- 崩溃和其他错误最常见的原因是在你的笔尖或故事板丢失或连接不良。这些通常不导致编译错误,因此可能会从视线中隐藏。
- 不要忽视编译器警告。如果你有,他们往往之所以出问题。如果你不明白为什么你得到一个特定的编译器警告,然后找出答案第一。这些都是生活储蓄!
- 在设备上进行调试,可以在模拟器上调试略有不同。这两个环境是不完全一样的,你会得到不同的结果。
例如,当我跑在我的iPhone 4应用程序的问题,第一个事故发生在NSArray的初始化,因为失踪的无定点,而不是因为应用程序调用SETLIST:在错误的视图控制器。这就是说,同样的原理也适用于寻找崩溃的根源。
而且不要忘了静态分析工具,这将赶上甚至更多的错误。如果你是一个初学者,我建议您始终启用它。为此,您可以为您的项目生成设置面板: