泰然教程组出品,转载请保留出处并通知泰然!翻译:大侠自来也;校对:Iven
原文地址:http://www.raywenderlich.com/10209/my-app-crashed-now-what-part-2
欢迎回到当程序崩溃的时候怎么办 教程!
在这个教程的第一部分,我们介绍了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,就像下面一样:
1
2
3
4
5
|
-
(
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”文字:
1
2
3
|
PProblems
[
18375
:
f803
]
You
tapped
on
:
&
lt
;
UIRoundedRectButton
:
0x6894800
;
frame
=
(
119
189
;
82
37
)
;
opaque
=
NO
;
autoresize
=
RM
+
BM
;
layer
=
&
lt
;
CALayer
:
0x68948f0
&
gt
;
&
gt
;
Problems
[
18375
:
f803
]
viewDidLoad
is
called
|
但是这次app将会再一次崩溃,但是却是一个新的问题。
注意:一旦你的代码好像没起什么什么作用的话,放置一些NSLog()在确切的地方,来看看是否这个方法是被执行了的和cpu通过怎么样路径执行这个方法。使用NSLog()来测试你假设将会执行的代码。
Assertion Failures
这个新的有趣的崩溃。它是一个SIGABRT,并且在调试窗口打印出来的是以下消息:
1
2
|
Problems
[
18375
:
f803
]
*
*
*
Assertion
failure
in
-
[
UITableView
_createPreparedCellForGlobalRow
:
withIndexPath
:
]
,
/
SourceCache
/
UIKit_Sim
/
UIKit
-
1912.3
/
UITableView
.
m
:
6072
|
我们得到的是一个执行UITableView的一些方法的一个“断言错误(assertion failure)”。当某些东西出错了之后,一个断言是一个内部相容性的检查器,并且会抛出一个异常。你也可以放置断言在你的代码里。例如:
1
2
3
4
5
6
|
-
(
void
)
doSomethingWithAString
:
(
NSString *
)
theString
{
NSAssert
(
string
!=
nil
,
@
"String cannot be nil"
)
;
NSAssert
(
[
string
length
]
&
gt
;
=
3
,
@
"String is too short"
)
;
.
.
.
}
|
在上面的方法里面,我们让一个NSString对象作为这个函数的变量,但是代码却不允许调用者传递一个nil或者长度小于3的字符串。假如这些条件中的一个不匹配的话,这个app将会终止,并且抛出一个异常。
你可以使用断言来作为一个防御性编程技术,因此你应该确定这个就是我们想要的代码行为。断言通常只在调试编译下有用的,因此他们对发布到app store的最终的app是没有运行时的影响的。
在这个情况下,某些情况触发了一个UITableView的断言错误,但是你并没有完全确定在那个地方。App也是停止在main.m里面,并且在执行堆栈里面只包含了框架(framework)的方法。
从这些方法的名字,我们可以猜测这个错误发生在重画这个tableview的某些地方。例如,我们可以看到layoutSubviews和_updateVisibleCellsNow:这些名字的方法。
继续运行这个app来看看是否可以得到一些比较好的错误消息—–记住,现在只是在抛出异常的时候暂停了程序,并没有崩溃。点击继续程序按钮,或者在调试窗口键入下面的命令:
1
|
(
lldb
)
c
|
你可能不得不多点击几次继续按钮,“c”命令也是一个简短的继续指令,和点击继续按钮一个效果,并不是就直接执行到最后。
现在这个调试窗口喷发出一些比较有用的信息:
1
2
3
4
5
6
7
|
*
*
*
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:方法返回一个有效的cell,因此在ListViewController.m方法里面增加一些调试输出信息来看看:
1
2
3
4
5
6
7
8
9
10
11
|
-
(
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()标记。再一次运行这个app,看看输出了什么:
1
|
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,现在这个调试窗口应该会打印出来:
1
2
3
4
5
6
|
Problems
[
7880
:
f803
]
the
cell
is
&
lt
;
UITableViewCell
:
0x6a6d120
;
frame
=
(
0
0
;
320
44
)
;
text
=
'Title'
;
layer
=
&
lt
;
CALayer
:
0x6a6d240
&
gt
;
&
gt
;
Problems
[
7880
:
f803
]
the
cell
is
&
lt
;
UITableViewCell
:
0x6877620
;
frame
=
(
0
0
;
320
44
)
;
text
=
'Title'
;
layer
=
&
lt
;
CALayer
:
0x6867140
&
gt
;
&
gt
;
Problems
[
7880
:
f803
]
the
cell
is
&
lt
;
UITableViewCell
:
0x6da1e80
;
frame
=
(
0
0
;
320
44
)
;
text
=
'Title'
;
layer
=
&
lt
;
CALayer
:
0x6d9fae0
&
gt
;
&
gt
;
Problems
[
7880
:
f803
]
the
cell
is
&
lt
;
UITableViewCell
:
0x6878c40
;
frame
=
(
0
0
;
320
44
)
;
text
=
'Title'
;
layer
=
&
lt
;
CALayer
:
0x6878f60
&
gt
;
&
gt
;
Problems
[
7880
:
f803
]
the
cell
is
&
lt
;
UITableViewCell
:
0x6da10c0
;
frame
=
(
0
0
;
320
44
)
;
text
=
'Title'
;
layer
=
&
lt
;
CALayer
:
0x6d9f240
&
gt
;
&
gt
;
Problems
[
7880
:
f803
]
the
cell
is
&
lt
;
UITableViewCell
:
0x6879640
;
frame
=
(
0
0
;
320
44
)
;
text
=
'Title'
;
layer
=
&
lt
;
CALayer
:
0x6878380
&
gt
;
&
gt
;
|
Verify Your Assumptions
你的NSLog()打印出来的消息,已经告诉我们6个table view cell被创建了,但是在table上面什么都看不见。怎么回事呢?假如你在模拟器里面到处点击一下,你将会注意到tableview中6个cell中的第一个却能够被选中。所以,显然cells都是存在的,只是他们都是空的:
是时候需要更多的调试记录了。将先前的NSLog()标记改变一下:
1
2
3
4
5
6
7
8
9
10
11
|
-
(
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
;
}
|
现在你打印出来就是你的数据模块的内容。运行这个app,看看显示出来的是什么:
1
2
3
4
5
6
|
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里面:
1
2
3
4
5
|
[
list
addObject
:
@
"One"
]
;
[
list
addObject
:
@
"Two"
]
;
[
list
addObject
:
@
"Three"
]
;
[
list
addObject
:
@
"Four"
]
;
[
list
addObject
:
@
"Five"
]
;
|
就像上面那样,这是测试你的假设是不是正确的一个很好的方法。可能你还想更准确的看看这个array里面到底有什么东西。改变先前在tableView:cellForRowAtIndexPath:里面的NSLog()为这样:
1
|
NSLog
(
@
"array contents: %@"
,
list
)
;
|
至少这样可以给你展示一些东西。运行这个app。假如你还没准备好猜测会发生什么情况,调试窗口已经给你打印出来了: