iOS指南系列:如何解决奔溃问题
iOS指南系列:如何解决奔溃问题-关于内存访问
iOS指南系列:如何解决奔溃问题-关于内存访问续
iOS指南系列:如何解决奔溃问题-关于内存访问续
iOS指南系列:如何解决奔溃问题-深入调试
iOS指南系列:程序运行非我所设想:tableview
iOS指南系列:如何解决内存问题 深入调试 结尾篇
Zombies!
接着上次修改后的程序,运行下,还是crash,关于内存的 EXC_BAD_ACCESS ,幸运的是调试器直接定位了代码行tableView:cellForRowAtIndexPath:
EXC_BAD_ACCESS 奔溃表明在你的代码中内存管理存在问题. 和SIGABRT 错误不一样, 很难得到明确的信息提示. 然而在这里,调试器确实有些选项可以帮助你检查内存问题(bingo,正如译者之前所言,也有类似的page guard等调试选项,帮助内存问题尽早暴露): Zombies!
打开 scheme editor 针对特定project的(也可以从菜单product):
选择Run action, 然后选 Diagnostics tab. 启用Enable Zombie Objects box:
运行程序command+R, 程序奔溃,得到如下信息 :
Problems[18702:f803] *** -[__NSArrayM objectAtIndex:]: message sent to deallocated instance 0x6d84980 |
Here’s what the Zombie Enabled tool does, in a nutshell: 简单介绍下zombie enable工具的作用:
每当你创建一个新的对象(它通过发送“malloc”消息),一大块的内存被保留并持有该对象的实例变量。当对象被释放,其引用数降为零,该内存被释放,以便其他对象可以在未来使用同一块内存区域。到目前为止,一切都很好。
然而,有时候,你仍然有指针指向现在已不存在的内存块,还是有一个有效的对象。如果你的程序的某些部分尝试使用stale的指针,应用程序将崩溃,导致EXC_BAD_ACCESS错误。
(它会崩溃,如果你是幸运的;如果你运气不好,应用程序将使用一个其他的对象并带来各种混乱,特别是如果该区域memory,在某些时候被一个新的对象覆盖,那么你就使用了错误的数据)
当zombie工具的启用,为对象的内存区域并没有在得到释放对象消息时被真实释放。相反,该内存会被标记为“undead”。如果您尝试稍后再次访问内存,应用程序可以承认自己的错误,它会中止并提示“消息发送到已释放的实例”的错误信息。这样可以有效帮助问题重现
所以这就是这里所发生的事情。这是一个undead对象的行:
cell.textLabel.text = [list objectAtIndex:indexPath.row]; |
结合下面一行信息,可以基本判断出问题的对象是NSArray list
-[__NSArrayM objectAtIndex:] |
undead对象的类是_ NSArrayM,如果你已经Cocoa编程的一段时间,你知道一些基础类,如NSString和NSArray,实际上是“类集群”,即原始类是 - NSString或NSArray的 - 被一个特殊的内部类取代。所以在这里,你可能会寻找一些NSArray的类型的对象,这是“list”(是一个定义为NSMutableArray类型的变量)。
如果你想确认下,可以通过nslog来判断list,当然也可以判断该变量的内存地址是否就是错误信息中提到的
NSLog(@"list is %p", list); |
通过%p,可以打印出指针的地址 (在我的测试中地址是 0x6d84980, 但你的测试结果往往显示不同的地址,我们的目的是判断该地址和错误信息的地址是否一致).
(lldb) p list |
Note: Unfortunately, this doesn’t appear to work properly for me with Xcode 4.3. For some reason, the address always shows up as 0×00000001, probably because of the class cluster.
With the GDB debugger, however, this works fine, and the variables pane in the debugger even points out that “list” is the zombie. So I’m assuming this is a bug in LLDB.
我们来检查下list的初始化分配代码 initWithCoder: 目前是这样的:
list = [NSMutableArray arrayWithCapacity:10]; |
由于当前项目没有启用 ARC (Automatic Reference Counting) –这表示我们需要自己管理变量/内存 – 如果在后续中使用,需要retain 这个变量:
// in initWithCoder: list = [[NSMutableArray arrayWithCapacity:10] retain]; |
To prevent a memory leak, you also have to release the object in dealloc as follows:
- (void)dealloc { [list release]; [super dealloc]; } |
再次运行程序。它仍然崩溃在同一行上,但是请注意,调试输出已改变:
Problems[8266:f803] array contents: ( One, Two, Three, Four, Five ) |
这意味着该阵列已正确分配和它包含的字符串。崩溃也不再是 EXC_BAD_ACCESS,但发出SIGABRT,你再挂异常断点,这就是debugger的工作:解决一个问题,找到另一个。 : - ]
Note: Even though such memory management-related errors are largely a thing of the past with ARC, you can still make your code crash on EXC_BAD_ACCESS errors, especially if you’re using unsafe_unretained properties and ivars.
My tip: Whenever you get an EXC_BAD_ACCESS error, enable Zombie Objects and try again.
Note that you shouldn’t leave Zombie Objects enabled all the time. Because this tool never deallocates memory, but simply marks it as being undead, you end up leaking all over the place and will run out of free memory at some point. So only enable Zombie Objects to diagnose a memory-related error, and then disable it again.
Stepping Through the App
Use a breakpoint to figure out this new problem. Put it on the line that is crashing:
这个问题通过断点以及连续调试,很容易得到结论,前几个cell没有问题,直到最后一个cell
按继续执行程序“按钮数次。在某一点上,应用程序崩溃,提示以下消息:
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 . . . |
如果通过 po来检查indexPath 对象, 你会发现:
(lldb) po indexPath (NSIndexPath *) $11 = 0x06a8a6c0 <NSIndexPath 0x6a8a6c0> 2 indexes [0, 5] |
节指数仍然是0,但行指数为5。请注意该错误消息也说,“索引”。由于从0开始计数,指数5,其实就是第六行。但是,在数据模型中只有五个项目!显然,认为有更多的行比实际上是表视图。
罪魁祸首,当然是下面的方法:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 6; } |
一般而言我们会写成如下的代码,适应不同的datasource变化
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [list count]; } |
删除或禁用断点,并再次运行应用程序。最后,表视图显示,有没有更多的崩溃!
Note: The “po” command is very useful for inspecting your objects. You can use it whenever your program is paused in the debugger, either after hitting a breakpoint or after it has crashed. You do need to make sure that the correct method is highlighted in the call stack, otherwise the debugger won’t be able to find the variable.
You can also see these variables in the debugger’s left pane, but often what you see there might take a bit of deciphering to figure out:
Once More, With Feeling
这样,程序就完全好了么?well,几乎是的,但当我swap某个cell(无论左右/右左),出现删除按钮后,点击会出现crash,断在代码tableView:commitEditingStyle:forRowAtIndexPath:.
The error message is:
Problems[18835:f803] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:1046 |
That looks like it comes from UIKit, not from the app’s code. Type “c” a few times to throw the exception so you get a more useful error message:
*** 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]; } } |
好极了!采取了一些努力,但你终于有了一个不崩溃的应用程序。 : - ]
Where to Go From Here?
Some key points to remember:
If your app crashes, the first thing is to figure out exactly where it crashed and why. Once you know these two things, fixing the crash is often easy. The debugger can help you with this, but you need to understand how to make it work for you.
Some crashes appear to happen randomly, and these are the tough ones, especially when you’re working with multiple threads. Most of the time, however, you can find a consistent way to make your app crash every time you try it.
If you can figure out how to reproduce the crash with a minimal number of steps, then you’ll also have a good way to verify that the bug was fixed (i.e. it won’t happen again). But if you cannot reliably reproduce the error, then you can never be certain that your changes have made it go away.
Tips:
- If the app crashes on main.m, then set the Exception Breakpoint.
- With the Exception Breakpoint enabled, you may no longer get a useful error message. In that case, either resume the app until you do, or type the “po $eax” command after the debug prompt.
- If you get an EXC_BAD_ACCESS, enable Zombie Objects and try again.
- The most common reason for crashes and other bugs are missing or bad connections in your nibs or storyboards. These usually don’t result in compiler errors and may therefore be hidden from sight.
- Don’t ignore compiler warnings. If you have them, they’re often the reason why things go wrong. If you don’t understand why you get a certain compiler warning, then figure that out first. These are life savers!
- Debugging on the device can be slightly different from debugging on the simulator. These two environments are not exactly the same and you’ll get different results.
For example, when I ran the Problems app on my iPhone 4, the very first crash happened in the NSArray initialization because of the missing nil sentinel, and not because the app was callingsetList: on the wrong view controller. That said, the same principles apply for finding the root causes of your crashes.
And don’t forget the static analyzer tool, which will catch even more mistakes. If you’re a beginner, I recommend that you always enable it. You can do this from the Build Settings panel for your project:
以上的原文总结非常好,翻译过来肯定不到位!
开开心心debugging! :-]
译者总结:
1.zoombie是有用的,但最终版本一定要disable(这个设置是项目级别的),否则程序性能问题
2.po ¥eax po variableSymbol p variablesymbol 基本的llbg调试指令:例如c
3.target build settings使用static analyzer来帮助分析代码潜在问题