https://i-blog.csdnimg.cn/blog_migrate/633f5668beaed252e7984b1e621fc5e6.png
This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on Google+ and Twitter.
Welcome back to the debugging tutorial!
The first part of the tutorial introduced SIGABRT and EXC_BAD_ACCESS errors, and illustrated some strategies for resolving them using the Xcode debugger and Exception Breakpoints.
But our app’s still got some problems! It doesn’t work exactly as it should, and there are plenty of crashes still lurking.
Fortunately, there are still more techniques you can learn to handle these problems that we’ll cover in this second and final part of this tutorial series.
So without further ado, let’s get right back to fixing this buggy app!
Getting Started: When What’s Supposed to Happen, Doesn’t
Where we left off in Part One, the app ran without crashing after a lot of debugging work. But it displayed an unexpectedly empty table, like so:
When you expect something to happen but it doesn’t, there are a few techniques you can use to troubleshoot. This tutorial will first look at using NSLog() to handle this.
The class for the table view controller is ListViewController. After the segue is performed, the app should load the ListViewController and show its view on the screen. You can test that assumption by making sure that the view controller’s methods are actually called. viewDidLoad seems like a good place for that.
In ListViewController.m, add an NSLog() to viewDidLoad as follows:
|
When you run the app, you should expect to see the text “viewDidLoad is called” in the Debug Pane after you press the Tap Me! button. Try it out. Not surprisingly, nothing appears in the Debug Pane. That means the ListViewController class isn’t used at all!
This usually means that you probably forgot to tell the storyboard that you want to use the ListViewController class for that table view controller scene.
Yep, the Class field in the Identity Inspector is set to the default value, UITableViewController. Change it to ListViewController and run the app again. Now the “viewDidLoad is called” text will appear in the Debug output:
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 |
The app will also crash again, but that’s a new problem.
Note: Whenever your code doesn’t appear to do anything, place a few NSLog() statements in strategic places, to see whether certain methods are actually being called and which path the CPU takes through these methods. Use NSLog() to test your assumptions about what the code does.
Assertion Failures
This new crash is a fun one. It’s a SIGABRT and the Debug Pane says the following:
Problems[18375:f803] *** Assertion failure in -[UITableView _createPreparedCellForGlobalRow: withIndexPath:], /SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:6072 |
We have an “assertion failure” that has something to do with a UITableView. An assertion is an internal consistency check that throws an exception when something is wrong. You can put assertions in your own code, too. For example:
|
The method above takes an NSString object as its parameter, but the code doesn’t allow callers to pass in nil or a string that has fewer than three characters. If either of these conditions is not met, the app will abort with an exception.
You use assertions as a defensive programming technique, so that you’re always sure the code behaves as expected. Assertions are usually enabled only in debug builds, so they have no runtime impact on the final app that is distributed on the App Store.
In this case, something triggered an assertion failure on UITableView, but you’re not entirely sure where yet. The app has paused on main.m and the call stack contains only framework methods.
From the names of these methods, you can guess that this error has something to do with redrawing the table view – for example, I see methods named layoutSubviews and _updateVisibleCellsNow:.
Continue running the app to see if you’re going to get a better error message – remember, you’re currently paused just before the exception will be thrown. Press the Continue Program Execution button, or type the following into the Debug Pane:
(lldb) c |
You may have to do this twice. The “c” command is short for continue and does the exact same thing as the Continue Program Execution button.
Now the Debug Pane spits out some more useful info:
*** 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 |
All right, this is a pretty good hint. Apparently the UITableView data source did not return a valid cell from tableView:cellForRowAtIndexPath:. So add some debugging output to that method in ListViewController.m as follows:
|
You added an NSLog() statement. Run the app again to see what it says.
Problems[18420:f803] the cell is (null) |
OK, so that means the call to dequeueReusableCellWithIdentifier: returned nil, something that only happens when the cell with the identifier “Cell” could not be found (because the app uses a storyboard with prototype cells).
Of course, this is a silly bug, and one that you no doubt would have solved a long time ago, because Xcode already warned about this via the handy compiler warnings: “Prototype cells must have have reuse identifiers.” I told you not to ignore those warnings! :P
Open the storyboard, select the prototype cell (the single cell at the top of the table view that says “Title”), and set its identifier to Cell:
With that fixed, all your compiler warnings should be gone. Run the app again and now the Debug Pane should say:
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>> |
Verify Your Assumptions
Your NSLog() shows that six table view cells were created, but still nothing is visible in the table. What gives? Well, if you tap around in the simulator for a bit, you’ll notice that the first six cells in the table view can actually be selected now. Apparently the cells are there but they’re just empty:
Time for some more debug logging. Change your previous NSLog() statement to:
|
Now you’re logging the contents of your data model. Run the app and check out what it says:
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) |
That explains why nothing shows up in the cells: because the text is always nil. However, if you check the code, at the top of the class in initWithStyle: you’re definitely putting strings into the list array:
|
In a case like this, it’s always a good idea to test your assumptions again. Maybe you should see what exactly is inside your array. Change the previous NSLog() for tableView:cellForRowAtIndexPath: to:
|
That should show you at least something. Run the app again. If you hadn’t already guessed, the debug output is:
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) |
Ah ha! A lightbulb goes off in your head. This was never going to work, because somebody forgot to actually allocate the array object in the first place. The “list” ivar was always nil, so calling addObject: and objectAtIndex: never had any effect at all.
You should allocate the list object when your view controller gets loaded, so inside initWithStyle: seems like a good place. Change that method to:
|
Give that a try. Whoops, still nothing! The debug output again says:
Problems[7971:f803] array contents: (null) . . . and so on . . . |
This sort of thing can be quite frustrating, but keep in mind that you’ll eventually get to the bottom of this if you have verified all the assumptions you’ve been making. So the question to ask right now is, does initWithStyle: actually get called?
Working With Breakpoints
You could put another NSLog() statement in the code, but there is another tool you can use as well: breakpoints. You’ve already seen the Exception Breakpoint, which pauses the app whenever an exception is thrown. You can also add other breakpoints, to virtually any place in your code. As soon as the program hits that spot, the breakpoint is triggered and the app jumps into the debugger.
You can set a breakpoint on a particular line in your code by clicking on the line number:
The blue arrow indicates that this line now has a breakpoint. You can also see this new breakpoint in the Breakpoint Navigator:
Run the app again. If initWithStyle: is indeed called, then the app should pause and jump into the debugger after you tap the “Tap Me!” button, when the ListViewController is loaded.
As you may have expected, no such thing happens. initWithStyle: is never called. That makes sense, of course, because the view controller is loaded from the storyboard (or a nib), in which case initWithCoder: is used instead.
Replace initWithStyle: with:
|
Keep the breakpoint on this method, just to see how this works:
As soon as you tap the button, the app jumps into the debugger:
This doesn’t mean the app has crashed! It’s merely paused at the position of the breakpoint. In the call stack on the left (if you don’t see the call stack, you might need to switch to the Debug Navigator) you can see that you got here from buttonTapped:. All the methods in between is the code that UIKit calls to perform a segue and load a new view controller. (Incidentally, breakpoints are a great tool to figure out how the system works internally.)
To continue running the app from where you left off, simply tap the Continue Program Execution button or type “c” in the Debug Console.
Of course, that doesn’t go as expected and the app crashes again. I told you it was a bit buggy!
Note: Before you continue, it’s a good idea to remove or disable the breakpoint on initWithCoder:. It has served its purpose, so now it can go away.
You can do this by right-clicking the breakpoint in the gutter (the area in the text editor with the line numbers) and choosing Delete Breakpoint from the pop-up menu. You can also drag the breakpoint out of the window, or you can remove it from the Breakpoint Navigator.
If you don’t want to remove the breakpoint just yet, you can simply disable it. To do that, you can either use the right-click menu or you can click once on the breakpoint – if the breakpoint indicator turns to a lighter shade of blue, it’s disabled.
But there’s another common bug in the initWithCoder: method. Can you find it?
Zombies!
Back to the crash. It’s an EXC_BAD_ACCESS and fortunately the debugger points to where it happens, inside tableView:cellForRowAtIndexPath:
That it’s an EXC_BAD_ACCESS crash means there’s a bug in your memory management. Unlike a SIGABRT, you won’t get a nice error message with such crashes. However, there is a debugging tool you can use that might shed some light on what’s going on here: Zombies!
Open the scheme editor for the project:
Select the Run action, and then the Diagnostics tab. Check the Enable Zombie Objects box:
Now, run the app again. The app still crashes, but now you’ll get the following error message:
Problems[18702:f803] *** -[__NSArrayM objectAtIndex:]: message sent to deallocated instance 0x6d84980 |
Here’s what the Zombie Enabled tool does, in a nutshell: whenever you create a new object (by sending it an “alloc” message), a chunk of memory is reserved to hold that object’s instance variables. When the object is released and its retain count hits zero, that memory gets deallocated so other objects can use it in the future. So far, so good.
However, it’s possible that you still have pointers that point at that now-defunct chunk of memory, under the assumption there’s still a valid object there. If some part of your program tries to use that stale pointer, the app will crash with an EXC_BAD_ACCESS error.
(It will crash, at least, if you’re lucky. If you’re unlucky, the app will use a dead object and all kinds of mayhem may ensue, especially if that memory at some point gets overwritten by a new object.)
When the Zombie tool is enabled, the memory for objects does not get deallocated when the object is released. Instead, that memory gets marked as being “undead.” If you try to access that memory again later, the app can recognize your mistake and it will abort with the “message sent to deallocated instance” error.
So that’s what is happening here. This is the line with an undead object:
|
The cell object and its textLabel are probably good, and so is the indexPath, so my guess is that the undead object in question here is “list.”
You already have a pretty good hint that it is indeed “list,” because the error message says:
-[__NSArrayM objectAtIndex:] |
The class of the undead object is __NSArrayM. If you’ve been programming with Cocoa for a while, you know that some of the Foundation classes such as NSString and NSArray are actually “class clusters,” meaning that the original class – NSString or NSArray – gets replaced by a special internal class. So here you’re probably looking at some NSArray-type object, which is exactly what “list” is (an NSMutableArray).
If you wanted to make sure, you could add an NSLog() after the line that allocates the “list” array:
|
This should print the same memory address as in the error message (in this case 0x6d84980, but when you try it the address will be different).
You should also be able to use the “p” command from the debugger to print out the address of the “list” variable (as opposed to the “po” command, which prints out the actual object, not its address). This saves you from the extra steps of having to add in the NSLog() statement and recompiling the app.
(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 0x00000001, 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.
The allocation for the list array in initWithCoder: currently looks like this:
|
Since this is not an ARC (Automatic Reference Counting) project – it uses manual memory management – you’ll need to retain this variable:
|
To prevent a memory leak, you also have to release the object in dealloc as follows:
|
Run the app again. It’s still crashing on the same line, but notice that the debug output has changed:
Problems[8266:f803] array contents: ( One, Two, Three, Four, Five ) |
This means that the array has been properly allocated and it contains the strings. The crash is also no longer an EXC_BAD_ACCESS but a SIGABRT, and you’re hung on your Exception Breakpoint again. Solve one problem, find another. :-]
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:
Run the app again and tap the button. You will now jump into the debugger the very first time tableView:cellForRowAtIndexPath: is called. Note that at this point the app hasn’t crashed yet, it’s just paused.
You want to figure out exactly when the app does crash. Press the Continue Program Execution button or type “c” behind the (lldb) prompt. This will resume the program from where you stopped it.
Nothing may appear to have happened – you’re still in tableView:cellForRowAtIndexPath: – but the Debug Pane now shows:
Problems[12540:f803] array contents: ( One, Two, Three, Four, Five ) |
That means tableView:cellForRowAtIndexPath: did execute once without any problems, because that NSLog() statement happened after the breakpoint. So the app was able to create the first cell just fine.
If you type the following into the debug prompt:
(lldb) po indexPath |
Then the output should be something like:
(NSIndexPath *) $3 = 0x06895680 <NSIndexPath 0x6895680> 2 indexes [0, 1] |
The important part is [0, 1]. This NSIndexPath object is apparently for section 0, row 1. In other words, the table view is now asking for the second row. From this, we can conclude that the app has no problems creating the cell for the first row, as the crash didn’t happen there.
Press the Continue Program Execution button several more times. At a certain point, the app does crash, with the following message:
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 . . . |
If you examine the indexPath object now, you’ll see:
(lldb) po indexPath (NSIndexPath *) $11 = 0x06a8a6c0 <NSIndexPath 0x6a8a6c0> 2 indexes [0, 5] |
The section index is still 0, but the row index is 5. Notice that the error message also says “index 5.” Because the counting starts at 0, index 5 actually means the sixth row. But there are only five items in the data model! Apparently the table view thinks there are more rows than there actually are.
The culprit, of course, is the following method:
|
It should really be written as:
|
Delete or disable the breakpoint and run the app again. Finally, the table view shows up and there are no more crashes!
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
Did I say no more crashes? Well, almost… Try a swipe-to-delete. The app now terminates on 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: . . . |
Ah, there you go. That’s pretty self-explanatory. The app tells the table view that a row was deleted, but someone forgot to remove it from the data model. As a result, the table view doesn’t see anything change. Fix the method to read as follows:
|
Excellent! It took some effort, but you finally have a crash-free app. :-]
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 calling setList: 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:
Have fun debugging! :-]
This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on Google+ and Twitter.
26 Comments
-
Hi,
Best tutorial .
Thanx . Nice way of explanation. -
"The first part of the tutorial introduced SIGABRT and EXC_BAD_ACCESS errors, and illustrated some strategies for resolving them using the Xcode debugger and Exception Breakpoints."
The hyper link for for "first part of the tutorial" in the tutorial is http://www.raywenderlich.com/10505/�http://www.raywenderlich.com/10209/my-app-crashed-now-what-part-1�
instead http://www.raywenderlich.com/10209/my-a ... hat-part-1 -
Thanks, extremely helpful.
One question - in the Debug Navigator after a crash when it shows the call stack, there are icons to the left of each item. Some look like a cup of coffee, some like a portrait, some like a gear, some like a text file. What do these various icons mean? -
Great posts! Thanks for writing them. I learned a lot.
-
Amazing tutorials......It really helped me
-
In the section "Assertion Failures" in the example method doSomethingWithAString: shouldn't the variable being referenced in the two NSAssert calls be the method argument theString rather than simply string?
-
brucehobbs Wrote:In the section "Assertion Failures" in the example method doSomethingWithAString: shouldn't the variable being referenced in the two NSAssert calls be the method argument theString rather than simply string?
Yes, you are correct. I'll fix it. Thanks for spotting this! -
Hi, thanks for the tutorial, I think the link for the first part of the tutorial you've included in the beginning is broken.
-
Hollance,
Thanks for the great tutorial! It looks like there is one more thing needs to be fixed:
In the following line of - (id)initWithCoder:(NSCoder *)aDecoder of ListViewController, "==" should be replaced by "=":
-
Code:
Select All Code
-
if (self == [super initWithCoder:aDecoder])
should be:
-
Code:
Select All Code
-
if (self = [super initWithCoder:aDecoder])
Regards,
David -
-
A very helpful post. There was a lot of new information for me. Especially using the console. Usually when I get an app crash, i go looking through the code immediately without paying much attention to the messages. So this should really help me save time.
-
Up until this moment, my only debugging tool has been NSLog. Thank you so much for this. Now it's time to do my best Rick Grimes and kill some zombies for my EXC_BAD_ACCESS error.
Ray's Monthly Newsletter
![](http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/images/global/starter-kit@2x.png)
Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!
![](https://i-blog.csdnimg.cn/blog_migrate/633f5668beaed252e7984b1e621fc5e6.png)