应用程序崩溃定位查找 (二)

教程的第一部分介绍了 SIGABRT 和 EXC_BAD_ACCESS 的错误,并说明解决他们使用 Xcode 调试器和异常断点的一些策略。

但我们的应用程序仍然有一些问题!它不能完全按照它应该并且有很多仍潜伏的崩溃。

幸运的是,还有更多的技术,你能学会如何处理这些问题,我们将在本系列教程的这第二个也是最后一部分。

所以事不宜迟,咱们马上回修复这个错误的应用程序!

入门教程: 当什么应该发生,并不

在我们离开在第一部分中,应用程序跑没有崩溃后大量的调试工作。但它显示意外为空的表,如下所示:

当你期待一些事情发生,但它不时,有几个你可以使用的技术来解决。本教程首先会看看使用NSLog()来处理这个问题。

为表视图控制器类是 ListViewController。执行界限后,应用程序应加载 ListViewController,显示其在屏幕上的视图。您可以通过确保实际上调用的视图控制器的方法来测试这种假设。viewDidLoad为那似乎是一个好地方。

ListViewController.m,添加NSLog() viewDidLoad ,如下所示:

- (void)viewDidLoad
{
	[super viewDidLoad];
	NSLog(@"viewDidLoad is called");
}

当您运行该应用程序时,你应该希望看到文本"viewDidLoad 被称为"在调试窗格中按水龙头我后!按钮。试试看。不令人惊讶的是,什么都不显示在调试窗格中。这意味着 ListViewController 类并不在所有使用!

这通常意味着您可能忘了告诉你想要使用 ListViewController 类的那个表视图控制器现场演示图板。

应用程序崩溃定位查找 (二)_第1张图片

是啊,在身份检查器中的类字段设置为默认值,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。断言是内部一致性检查,有什么不对劲时引发异常。你也可以在您自己的代码,把断言。例如:

- (void)doSomethingWithAString:(NSString *)theString
{
	NSAssert(theString != nil, @"String cannot be nil");
	NSAssert([theString length] >= 3, @"String is too short");
	. . .
}

上述方法需要一个 NSString 对象作为其参数,但代码不允许调用方在零通或具有少于三个字符的字符串。如果这些条件不满足,应用程序将中止处理的异常。

你使用断言作为一种防御性的编程技术,所以,你总是确保代码行为与预期相同。所以他们对分布在 App Store 的最终应用程序没有运行时影响,断言通常只在调试版本中,已启用。

在这种情况下,东西触发了断言失败上 UITableView,但你不在尚未完全确定。应用程序已暂停对main.m和调用堆栈只包含框架方法。

从这些方法的名称,你可以猜到此错误有某事与重绘的表视图 — — 例如,我看到了名为layoutSubviews的方法和_updateVisibleCellsNow:.

继续运行该应用程序,看看是否你要得到更好的错误消息 — — 请记住,你目前暂停之前,将引发异常。按下继续程序执行按钮,或到调试窗格中键入以下内容:

(lldb) c

你可能会有两次做到这一点。"C"命令是简称继续并作为继续程序执行按钮同样的事情。

现在调试窗格中吐出一些更有用的信息:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', 
reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
*** First throw call stack:
(0x13ba052 0x154bd0a 0x1362a78 0x99a2db 0xaaee3 0xab589 0x96dfd 0xa5851 0x50301 
0x13bbe72 0x1d6492d 0x1d6e827 0x1cf4fa7 0x1cf6ea6 0x1cf6580 0x138e9ce 0x1325670
0x12f14f6 0x12f0db4 0x12f0ccb 0x12a3879 0x12a393e 0x11a9b 0x2722 0x2695)
terminate called throwing an exception

好吧,这是一个很好的提示。显然 UITableView 数据源未返回有效的单元格,从tableView:cellForRowAtIndexPath:所以一些调试将输出添加到ListViewController.m中的该方法,如下所示:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
 
	NSLog(@"the cell is %@", cell);
 
	cell.textLabel.text = [list objectAtIndex:indexPath.row];
 
	return cell;
}

您添加的NSLog() 语句运行该应用程序,再来看看它怎么说。

Problems[18420:f803] the cell is (null)

好吧,那意味着对的调用dequeueReusableCellWithIdentifier:返回 nil,只有当无法找到具有标识符"细胞"的单元格,(因为该应用程序使用演示图板与原型细胞) 的东西。

当然,这是一个愚蠢的错误和那个你无疑会解决很久以前,因为 Xcode 已经警告这通过方便编译器警告:"原型细胞必须有重用标识符"。我告诉你不要忽略这些警告!: P

打开演示图板,选择原型细胞 (单个单元格显示为"标题"的表视图顶部),并将其标识符设置为单元格:

应用程序崩溃定位查找 (二)_第2张图片

与固定的所有你的编译器警告应该会消失。再次运行该应用程序,现在应该说调试窗格中:

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()显示六个表格视图单元格被创造了,但仍然什么也看不见在表中。谁给了?好吧,如果你点击周围在模拟器中的一点,你会注意到,现在实际上可以选择表视图中的前六个单元格。显然,细胞有,但他们只是空:

应用程序崩溃定位查找 (二)_第3张图片

一些更多的调试日志记录的时间。更改到你以前的 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:你肯定把字符串到列表的数组:

		[list addObject:@"One"];
		[list addObject:@"Two"];
		[list addObject:@"Three"];
		[list addObject:@"Four"];
		[list addObject:@"Five"];

在这种情况下,它始终是一个好主意,再次测试你的假设。也许你应该看什么到底里面您的阵列。改变以前的NSLog()tableView:cellForRowAtIndexPath:到:

	NSLog(@"array contents: %@", list);

那应该至少东西给你看。再次运行该应用程序。如果你已经没猜到,调试输出结果:

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:看起来像一个好地方。更改对该方法:

- (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"];
	}
	return self;
}

试一试。哎呀,还是没什么!调试输出再次说:

Problems[7971:f803] array contents: (null)
. . . and so on . . .

这样的事情可以相当令人沮丧,但请记住,最终你会得到的这底部如果您已验证所有的假设条件做了。所以要问现在的问题是,是否initWithStyle:实际上会被调用吗?

使用断点

你可以把另一个NSLog()语句放在代码中,但您也可以使用另一个工具: 断点。你已经看到了异常断点,暂停该应用程序时引发的异常。也可以将其他断点添加到几乎任何地方在您的代码。程序打那个位置,触发断点,该应用程序跳到调试器。

您可以设置断点在特定的行在代码中通过点击行号:

应用程序崩溃定位查找 (二)_第4张图片

蓝色箭头指示这种线现在有一个断点。您还可以看到此新断点断点导航器中:

再次运行该应用程序。如果initWithStyle:的确调用时,应用程序应该暂停,然后跳到调试器后你点击"点击我!"按钮,当加载 ListViewController。

正如您可能已经预料,没有这种事情发生。initWithStyle:永远不会调用。有道理,当然,因为视图控制器从演示图板 (或笔尖),在这种情况下加载initWithCoder:来代替。

取代initWithStyle:与:

- (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"];
	}
	return self;
}

这种方法,只是为了看看这是如何保持断点:

应用程序崩溃定位查找 (二)_第5张图片

只要你点击按钮,应用程序跳到调试器:

这并不意味着应用程序已经崩溃!只是,它已经暂停在断点的位置。在左边的调用堆栈中 (如果你看不到调用堆栈,您可能需要切换到调试导航) 你可以看到你在这里从buttonTapped:在之间的所有方法都是 UIKit 调用以执行界限并加载一个新的视图控制器的代码。(顺便说一句,断点是一个伟大的工具,以找出系统内部的工作方式)

若要继续运行该应用程序从你停下来的地方,点击继续程序执行按钮或者"c"型在调试控制台中。

当然,这不会按预期和应用程序再次崩溃。我告诉你这是有点马车!

注:在继续之前,它是一个好主意,移除或禁用断点上initWithCoder:它已达到其目的,所以现在它可以消失。

你可以通过右键单击断点在阴沟里 (在带有行号的文本编辑器区域) 并从弹出式菜单中选择删除断点。您还可以拖动断点窗外,或你可以去除断点导航。

如果你不想要只是还没有删除断点,你可以简单地禁用它。要做到这一点,你可以使用右键单击菜单或您可以单击一旦在该断点处 — — 如果断点指示器变为淡蓝色的它将被禁用。

但还有另一个常见的 bug initWithCoder:方法。你能找到它吗?

僵尸!

追溯到崩溃。它是 EXC_BAD_ACCESS,幸运的是它发生的地方,里面 tableView:cellForRowAtIndexPath 调试器指向:

应用程序崩溃定位查找 (二)_第6张图片

它是 EXC_BAD_ACCESS 的崩溃意味着有只虫子在你内存管理。不像 SIGABRT,你不会得到与此类撞车的很好的错误消息。然而,还有一种可能揭示一些什么怎么回事你可以使用的调试工具: 僵尸!

打开项目的计划编辑器:

选择运行操作,然后在诊断选项卡复选启用僵尸对象框中:

应用程序崩溃定位查找 (二)_第7张图片

现在,再次运行该应用程序。应用程序仍然崩溃,但现在你会得到以下错误消息:

Problems[18702:f803] *** -[__NSArrayM objectAtIndex:]: message sent to deallocated instance 0x6d84980

这里是僵尸启用工具的功能,概括地说: 每次创建一个新的对象 (通过它发送一条"分配"消息),保留一块内存以保存该对象的实例变量。当该对象被释放和其保留计数减到零,那内存获取释放所以其他对象可以使用它在将来。目前为止,一切都好。

然而,它是内存的可能你还有点,现已解散块的指针,在假设条件下仍然是内存的有效的对象。如果您的程序的某些部分尝试使用那陈旧的指针,该应用程序将会崩溃,EXC_BAD_ACCESS 错误。

(它会崩溃,至少,如果你幸运的话。如果你运气不好,应用程序将使用一个死的对象,各种各样的混乱可能紧随其后,尤其是如果该内存在一些点获取一个新的对象被覆盖)

僵尸工具启用时,对象的内存,并不获得释放,当该对象被释放。相反,该内存获取标记为正在"活死人"。如果您尝试访问该内存以后再这个软件可以识别你的错误,它将中止与"消息发送到释放实例"的错误。

这就是这里发生了什么。这是与亡灵的对象的行:

	cell.textLabel.text = [list objectAtIndex:indexPath.row];

单元格对象和它的 textLabel 是可能好,并且因此是 indexPath,所以我的猜测是,亡灵的对象问题在这里是"名单"。

你已经有很好的暗示,它是确实"名单",因为错误消息说:

-[__NSArrayM objectAtIndex:]

亡灵的类是对象的 __NSArrayM。如果你使用可可编程已一段时间后,你知道一些基础类如 NSString 和 NSArray 是实际上"类集群,"意思原始类 — — NSString 或 NSArray — — 获取替换为一种特殊的内部类。所以在这里你可能看一些 NSArray 类型的对象,这就是到底什么"列表"(NSMutableArray) 了。

如果你想要确保,你可以分配"列表"数组的行后面添加NSLog() :

NSLog(@"list is %p", list);

这应该打印错误消息相同的内存地址 (在这情况下 0x6d84980,但当你试一试的地址将是不同)。

你也应该能够使用调试器的"p"命令打印出变量的地址"列表"(而不是"宝"命令,该命令打印出的实际的对象,不是它的地址)。这使你免于不得不在NSLog()语句中添加和重新编译应用程序的额外步骤。

(lldb) p list

注:不幸的是,这似乎并没有对我来说正常工作,与 Xcode 4.3。出于某种原因,地址总是显示为 0x00000001,可能由于类群集。

使用 GDB 调试器,然而,这工作正常,并在调试器中的变量窗格甚至指出"列表"是僵尸。所以我假设这一个在 LLDB 的 bug。

为中的列表数组分配initWithCoder:目前看上去像这样:

		list = [NSMutableArray arrayWithCapacity:10];

因为这不是一个弧 (自动引用计数) 项目 — — 它使用手动内存管理 — — 你需要保留此变量:

// in initWithCoder:
		list = [[NSMutableArray arrayWithCapacity:10] retain];

为了防止内存泄漏,你还必须释放 dealloc 中的对象,如下所示:

- (void)dealloc
{
	[list release];
	[super dealloc];
}

再次运行该应用程序。它仍然坠毁在同一行,但调试输出已更改的通知:

Problems[8266:f803] array contents: (
    One,
    Two,
    Three,
    Four,
    Five
)

这意味着数组编配妥当和它包含的字符串。飞机坠毁也不再是 EXC_BAD_ACCESS SIGABRT,和你再一次就挂在你异常断点。解决一个问题,找到另一个。:-]

注:即使这种内存管理相关的错误主要是过去的事情与弧,你仍然可以你代码崩溃 EXC_BAD_ACCESS 错误,尤其是如果您使用 unsafe_unretained 属性和高德。

我的提示: 每当你得到一个 EXC_BAD_ACCESS 错误,使僵尸的对象,然后重试。

注意,你不应该离开僵尸对象启用所有的时间。因为此工具永远不会释放内存,但只是将它标记为被不死族,你最终到处漏水和某一时刻会耗尽可用内存。因此,只有使僵尸对象来诊断与内存相关的错误,然后再次禁用它。

逐句通过该应用程序

使用断点来找出这个新的问题。把它放在崩溃的行:

应用程序崩溃定位查找 (二)_第8张图片

再次运行该应用程序,然后点击按钮。你现在会跳进调试器很第一次tableView:cellForRowAtIndexPath:调用。请注意,此时应用程序还没摔,它只是已经暂停。

你想要找出到底当应用程序执行崩溃。按继续程序执行按钮或键入"c"的背后 (lldb) 提示符。这将恢复程序从停止的位置。

什么都不可能会出现 — — 你仍在tableView:cellForRowAtIndexPath: — — 但调试窗格现在显示:

Problems[12540:f803] array contents: (
    One,
    Two,
    Three,
    Four,
    Five
)

这意味着tableView:cellForRowAtIndexPath:执行了一次没有任何问题,因为在断点后发生的那NSLog()语句。这样应用程序就能创建第一个单元格就好。

如果您在调试提示符下键入以下:

(lldb) po indexPath

然后输出应类似于:

(NSIndexPath *) $3 = 0x06895680 <NSIndexPath 0x6895680> 2 indexes [0, 1]

重要的部分是 [0,1]。此 NSIndexPath 对象显然为 0,第 1 行一节。换句话说,表视图现正寻求第二行。由此,我们可以得出结论应用程序已创建的第一行的单元格中没有问题,如崩溃并没有发生。

按继续程序执行按钮几次。在某一个点,应用程序会崩溃的以下消息:

Problems[12540:f803] *** Terminating app due to uncaught exception 'NSRangeException', 
reason: '*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 4]'
*** First throw call stack:
. . . and so on . . .

如果你现在检查 indexPath 对象,你会看到:

(lldb) po indexPath
 
(NSIndexPath *) $11 = 0x06a8a6c0 <NSIndexPath 0x6a8a6c0> 2 indexes [0, 5]

部分索引仍然是 0,但是行索引是 5。注意错误消息还说:"指数 5"。从 0 开始计数,因为索引 5 实际上意味着第六行。但在数据模型中只有五个项目!显然的表视图认为还有比实际上有更多的行。

罪魁祸首,当然,是下面的方法:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return 6;
}

它真的应该写成:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

删除或禁用断点并再次运行该应用程序。最后,表视图显示,有没有更多的碰撞!

注:"Po"命令是非常有用的检查您的对象。每当您的程序暂停在调试器中后命中断点, 或之后已经崩溃,你可以使用它。你需要确保正确的方法在调用堆栈中突出显示,否则,调试器将无法找到该变量。

您还可以看到这些变量在调试器的左窗格中,但往往你看到了什么那里可能需要一点的破译弄清:

应用程序崩溃定位查找 (二)_第9张图片

再一次的感觉

我说没有更多的碰撞吗?好吧,几乎...试着刷卡删除。应用程序现在将终止对tableView:commitEditingStyle:forRowAtIndexPath:.

错误消息是:

Problems[18835:f803] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], 
/SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:1046

看起来好像它来自 UIKit,不是从应用程序的代码。"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).'
*** First throw call stack: . . .

啊,你在那边。这是不言自明的。应用程序可以告诉表视图的行已被删除,但有人忘了从数据模型中移除它。因此,表视图不认为有什么改变。解决方法如下:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
	if (editingStyle == UITableViewCellEditingStyleDelete)
	{
		[list removeObjectAtIndex:indexPath.row];
		[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
	}   
}

非常好!花了一些努力,但你最后有无故障的应用程序。:-]

要从这里去哪里?

要记住一些关键点:

如果您的应用程序崩溃,第一件事是要弄清楚到底哪里它坠毁和为什么。一旦你知道这两件事,是经常容易修复崩溃。调试器可以帮你,但是你需要了解如何使它为你工作。

一些崩溃似乎随机,发生,这些照片是艰难的尤其是当您正在使用多个线程。大部分的时间,然而,你可以找到一致的方法来使您的应用程序崩溃,每次你试一试。

如果你能弄清楚如何复制在车祸中很少的步骤,那么你也会有验证 bug 修复的好方法 (即它不会再发生)。但是,如果你不能可靠地重现的错误,那么你可以永远不能确定您的更改已消失。

小贴士:

  • 如果应用程序崩溃在main.m上,然后设置异常断点。
  • 与启用了异常断点,你可能不再得到有用的错误消息。在这种情况下,要么恢复应用程序,直到你做,或在调试提示符后键入"po $eax"命令。
  • 如果你得到 EXC_BAD_ACCESS,使僵尸的对象,然后再试。
  • 崩溃和其他 bug 的最常见原因是缺少或坏你家发钞银行或演示图板中的连接。这些通常不会导致编译器错误,因此可能从我眼前隐藏。
  • 不要忽视编译器警告。如果你有他们,他们常常是为什么事情出错的原因。如果你不明白为什么你得到某些编译器警告,然后图那出来的第一。这些都是大救星!
  • 在设备上调试可以从调试在模拟器上略有不同。这两种环境都不是一样,你会得到不同的结果。

    例如,当我在我的 iPhone 4 上运行问题的应用程序,,第一次事故发生在 NSArray 初始化因为缺少零的哨兵,并不是因为该应用程序被调用曲目:错视图控制器上。这就是说,原则同样适用于找到你崩溃的根本原因。

并且不要忘记的静态分析器工具,会犯更多错误。如果你是一个初学者,我建议您始终启用它。你可以从生成设置面板为您的项目:

应用程序崩溃定位查找 (二)_第10张图片

你可能感兴趣的:(测试,Bug修复,崩溃处理,ios警告)